500
This commit is contained in:
@@ -21,6 +21,7 @@ import { ProfilePage } from '@/pages/ProfilePage'
|
||||
import { UserProfilePage } from '@/pages/UserProfilePage'
|
||||
import { NotFoundPage } from '@/pages/NotFoundPage'
|
||||
import { TeapotPage } from '@/pages/TeapotPage'
|
||||
import { ServerErrorPage } from '@/pages/ServerErrorPage'
|
||||
|
||||
// Protected route wrapper
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
@@ -154,6 +155,10 @@ function App() {
|
||||
<Route path="teapot" element={<TeapotPage />} />
|
||||
<Route path="tea" element={<TeapotPage />} />
|
||||
|
||||
{/* Server error page */}
|
||||
<Route path="500" element={<ServerErrorPage />} />
|
||||
<Route path="error" element={<ServerErrorPage />} />
|
||||
|
||||
{/* 404 - must be last */}
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -22,11 +22,28 @@ client.interceptors.request.use((config) => {
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError<{ detail: string }>) => {
|
||||
// Unauthorized - redirect to login
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
// Server error or network error - redirect to 500 page
|
||||
if (
|
||||
error.response?.status === 500 ||
|
||||
error.response?.status === 502 ||
|
||||
error.response?.status === 503 ||
|
||||
error.response?.status === 504 ||
|
||||
error.code === 'ERR_NETWORK' ||
|
||||
error.code === 'ECONNABORTED'
|
||||
) {
|
||||
// Only redirect if not already on error page
|
||||
if (!window.location.pathname.startsWith('/500') && !window.location.pathname.startsWith('/error')) {
|
||||
window.location.href = '/500'
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
143
frontend/src/pages/ServerErrorPage.tsx
Normal file
143
frontend/src/pages/ServerErrorPage.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { NeonButton } from '@/components/ui'
|
||||
import { Home, Sparkles, RefreshCw, ServerCrash, Flame, Zap } from 'lucide-react'
|
||||
|
||||
export function ServerErrorPage() {
|
||||
const handleRefresh = () => {
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-[70vh] flex flex-col items-center justify-center text-center px-4">
|
||||
{/* Background effects */}
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 -left-32 w-96 h-96 bg-red-500/5 rounded-full blur-[100px]" />
|
||||
<div className="absolute bottom-1/4 -right-32 w-96 h-96 bg-orange-500/5 rounded-full blur-[100px]" />
|
||||
</div>
|
||||
|
||||
{/* Server icon */}
|
||||
<div className="relative mb-8">
|
||||
{/* Smoke/fire effect */}
|
||||
<div className="absolute -top-6 left-1/2 -translate-x-1/2 flex gap-3">
|
||||
<Flame className="w-6 h-6 text-orange-500/60 animate-flicker" style={{ animationDelay: '0s' }} />
|
||||
<Flame className="w-5 h-5 text-red-500/50 animate-flicker" style={{ animationDelay: '0.2s' }} />
|
||||
<Flame className="w-6 h-6 text-orange-500/60 animate-flicker" style={{ animationDelay: '0.4s' }} />
|
||||
</div>
|
||||
|
||||
{/* Server with error */}
|
||||
<div className="relative">
|
||||
<div className="w-32 h-32 rounded-2xl bg-dark-700/80 border-2 border-red-500/30 flex items-center justify-center shadow-[0_0_30px_rgba(239,68,68,0.2)]">
|
||||
<ServerCrash className="w-16 h-16 text-red-400" />
|
||||
</div>
|
||||
|
||||
{/* Error indicator */}
|
||||
<div className="absolute -bottom-2 -right-2 w-10 h-10 rounded-xl bg-red-500/20 border border-red-500/40 flex items-center justify-center animate-pulse">
|
||||
<Zap className="w-5 h-5 text-red-400" />
|
||||
</div>
|
||||
|
||||
{/* Sparks */}
|
||||
<div className="absolute top-2 -left-3 w-2 h-2 rounded-full bg-yellow-400 animate-spark" style={{ animationDelay: '0s' }} />
|
||||
<div className="absolute top-6 -right-2 w-1.5 h-1.5 rounded-full bg-orange-400 animate-spark" style={{ animationDelay: '0.3s' }} />
|
||||
<div className="absolute bottom-4 -left-2 w-1.5 h-1.5 rounded-full bg-red-400 animate-spark" style={{ animationDelay: '0.6s' }} />
|
||||
</div>
|
||||
|
||||
{/* Glow effect */}
|
||||
<div className="absolute inset-0 bg-red-500/20 rounded-full blur-3xl -z-10" />
|
||||
</div>
|
||||
|
||||
{/* 500 text */}
|
||||
<div className="relative mb-4">
|
||||
<h1 className="text-8xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-red-400 via-orange-400 to-yellow-400">
|
||||
500
|
||||
</h1>
|
||||
<div className="absolute inset-0 text-8xl font-bold text-red-500/20 blur-xl">
|
||||
500
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-white mb-3">
|
||||
Ошибка сервера
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-400 mb-2 max-w-md">
|
||||
Что-то пошло не так на нашей стороне.
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm mb-8 max-w-md">
|
||||
Мы уже работаем над решением проблемы. Попробуйте обновить страницу.
|
||||
</p>
|
||||
|
||||
{/* Status info */}
|
||||
<div className="glass rounded-xl p-4 mb-8 max-w-md border border-red-500/20">
|
||||
<div className="flex items-center gap-2 text-red-400 mb-2">
|
||||
<ServerCrash className="w-4 h-4" />
|
||||
<span className="text-sm font-semibold">Internal Server Error</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Сервер временно недоступен или перегружен. Обычно это быстро исправляется.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-4">
|
||||
<NeonButton
|
||||
size="lg"
|
||||
icon={<RefreshCw className="w-5 h-5" />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Обновить
|
||||
</NeonButton>
|
||||
<Link to="/">
|
||||
<NeonButton size="lg" variant="secondary" icon={<Home className="w-5 h-5" />}>
|
||||
На главную
|
||||
</NeonButton>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Decorative sparkles */}
|
||||
<div className="absolute top-1/4 left-1/4 opacity-20">
|
||||
<Sparkles className="w-6 h-6 text-red-400 animate-pulse" />
|
||||
</div>
|
||||
<div className="absolute bottom-1/3 right-1/4 opacity-20">
|
||||
<Sparkles className="w-4 h-4 text-orange-400 animate-pulse" style={{ animationDelay: '1s' }} />
|
||||
</div>
|
||||
|
||||
{/* Custom animations */}
|
||||
<style>{`
|
||||
@keyframes flicker {
|
||||
0%, 100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-3px) scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-1px) scale(0.9);
|
||||
opacity: 0.5;
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-4px) scale(1.05);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.animate-flicker {
|
||||
animation: flicker 0.8s ease-in-out infinite;
|
||||
}
|
||||
@keyframes spark {
|
||||
0%, 100% {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.animate-spark {
|
||||
animation: spark 1.5s ease-in-out infinite;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,3 +11,4 @@ export { ProfilePage } from './ProfilePage'
|
||||
export { UserProfilePage } from './UserProfilePage'
|
||||
export { NotFoundPage } from './NotFoundPage'
|
||||
export { TeapotPage } from './TeapotPage'
|
||||
export { ServerErrorPage } from './ServerErrorPage'
|
||||
|
||||
Reference in New Issue
Block a user