From 40378ad29277ab201e5c1b06da93243258c0f215 Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Tue, 22 Sep 2026 00:53:42 +0300 Subject: [PATCH] feat(frontend): implement startup server ping and fallback to 500 error page --- frontend/src/App.tsx | 37 +++++++++++++---- frontend/src/config/api.ts | 5 +++ frontend/src/pages/Login/Login.tsx | 53 ++++++++++++++++++++----- frontend/src/services/apiClient.ts | 64 ++++++++++++++++++++++++++++++ frontend/tsconfig.tsbuildinfo | 2 +- 5 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 frontend/src/config/api.ts create mode 100644 frontend/src/services/apiClient.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b9aa348..0cc57ae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,16 +1,39 @@ -import { Routes, Route } from 'react-router-dom'; +import { useEffect } from 'react'; +import { Routes, Route, useNavigate, useLocation } from 'react-router-dom'; import Login from './pages/Login/Login'; import ErrorPage from './pages/ErrorPage/ErrorPage'; +import { pingServer, registerServerDownHandler } from './services/apiClient'; export default function App() { + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + // Register global handler for any API fault + registerServerDownHandler(() => { + navigate('/500', { replace: true }); + }); + + // Skip ping if already on an error route + if (location.pathname === '/500' || location.pathname === '/403' || location.pathname === '/404' || location.pathname === '/422') { + return; + } + + // Startup ping to verify server connectivity + pingServer().then((isOnline) => { + if (!isOnline) { + navigate('/500', { replace: true }); + } + }); + }, [navigate, location.pathname]); + return ( - } /> - } /> - } /> - } /> - } /> - + } /> + } /> + } /> + } /> + } /> ); } diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts new file mode 100644 index 0000000..1fa43a5 --- /dev/null +++ b/frontend/src/config/api.ts @@ -0,0 +1,5 @@ +// Central API Configuration +// Currently points to placeholder URL until server configuration is shared +export const API_BASE_URL: string = + (import.meta as { env?: { VITE_API_BASE_URL?: string } }).env?.VITE_API_BASE_URL || + 'https://example.com'; diff --git a/frontend/src/pages/Login/Login.tsx b/frontend/src/pages/Login/Login.tsx index 7181b30..8abd43b 100644 --- a/frontend/src/pages/Login/Login.tsx +++ b/frontend/src/pages/Login/Login.tsx @@ -1,6 +1,7 @@ import { FormEvent, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styles from './Login.module.scss'; +import { apiFetch } from '../../services/apiClient'; type Mode = 'signin' | 'signup'; @@ -11,6 +12,7 @@ export default function Login() { const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); const navigate = useNavigate(); function toggleMode() { @@ -21,18 +23,47 @@ export default function Login() { async function handleSubmit(e: FormEvent) { e.preventDefault(); setError(null); + setLoading(true); - if (mode === 'signup') { - if (password !== confirmPassword) { - setError('Passwords do not match.'); - return; + try { + if (mode === 'signup') { + if (password !== confirmPassword) { + setError('Passwords do not match.'); + setLoading(false); + return; + } + const res = await apiFetch('/api/auth/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, email, password }), + }); + const data = await res.json(); + if (!data.success) { + setError(data.message || 'Signup failed.'); + setLoading(false); + return; + } + } else { + const res = await apiFetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + const data = await res.json(); + if (!data.success) { + setError(data.message || 'Login failed.'); + setLoading(false); + return; + } } - // await signup(name, email, password); - } else { - // await login(email, password); - } - navigate('/profile'); + navigate('/'); + } catch { + // apiFetch automatically triggers server-down navigation on network fault + setError('Unable to reach server.'); + } finally { + setLoading(false); + } } return ( @@ -78,7 +109,9 @@ export default function Login() { {error &&

{error}

} - +

{mode === 'signin' ? "Don't have an account?" : 'Already have an account?'}{' '} diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts new file mode 100644 index 0000000..5470c0c --- /dev/null +++ b/frontend/src/services/apiClient.ts @@ -0,0 +1,64 @@ +import { API_BASE_URL } from '../config/api'; + +export class ServerDownError extends Error { + constructor(message = 'Server is unreachable') { + super(message); + this.name = 'ServerDownError'; + } +} + +// Global listener for server unreachability / fault +let onServerDownHandler: (() => void) | null = null; + +export function registerServerDownHandler(handler: () => void) { + onServerDownHandler = handler; +} + +export function triggerServerDown() { + if (onServerDownHandler) { + onServerDownHandler(); + } +} + +/** + * Pings the server health/status endpoint. + * Returns true if server responds with 2xx status, false otherwise. + */ +export async function pingServer(timeoutMs = 3500): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const res = await fetch(`${API_BASE_URL}/api/auth/status`, { + method: 'GET', + signal: controller.signal, + }); + clearTimeout(timer); + return res.ok; + } catch { + clearTimeout(timer); + return false; + } +} + +/** + * Global wrapper around fetch for server API communication. + * Automatically triggers server-down navigation if the server is unreachable or faults. + */ +export async function apiFetch(endpoint: string, options: RequestInit = {}): Promise { + const url = endpoint.startsWith('http') ? endpoint : `${API_BASE_URL}${endpoint}`; + try { + const response = await fetch(url, options); + if (response.status >= 500) { + triggerServerDown(); + throw new ServerDownError(`Server error (HTTP ${response.status})`); + } + return response; + } catch (err) { + triggerServerDown(); + if (err instanceof ServerDownError) { + throw err; + } + throw new ServerDownError(err instanceof Error ? err.message : 'Network error'); + } +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 70a4da1..98ed9a7 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/context/themecontext.tsx","./src/pages/errorpage/errorpage.tsx","./src/pages/login/login.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/config/api.ts","./src/context/themecontext.tsx","./src/pages/errorpage/errorpage.tsx","./src/pages/login/login.tsx","./src/services/apiclient.ts"],"version":"5.9.3"} \ No newline at end of file