Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Routes>
<Route path="*" element={<Login />} />
<Route path="/403" element={<ErrorPage kind="not-authorized" />} />
<Route path="/500" element={<ErrorPage kind="server-down" />} />
<Route path="/422" element={<ErrorPage kind="file-corrupted" />} />
<Route path="/404" element={<ErrorPage kind="not-found" />} />

<Route path="/500" element={<ErrorPage kind="server-down" />} />
<Route path="/403" element={<ErrorPage kind="not-authorized" />} />
<Route path="/422" element={<ErrorPage kind="file-corrupted" />} />
<Route path="/404" element={<ErrorPage kind="not-found" />} />
<Route path="*" element={<Login />} />
</Routes>
);
}
5 changes: 5 additions & 0 deletions frontend/src/config/api.ts
Original file line number Diff line number Diff line change
@@ -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';
53 changes: 43 additions & 10 deletions frontend/src/pages/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,6 +12,7 @@ export default function Login() {
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();

function toggleMode() {
Expand All @@ -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 (
Expand Down Expand Up @@ -78,7 +109,9 @@ export default function Login() {

{error && <p className={styles.error}>{error}</p>}

<button type="submit">{mode === 'signin' ? 'Sign In' : 'Sign Up'}</button>
<button type="submit" disabled={loading}>
{loading ? 'Please wait...' : mode === 'signin' ? 'Sign In' : 'Sign Up'}
</button>

<p className={styles.switch}>
{mode === 'signin' ? "Don't have an account?" : 'Already have an account?'}{' '}
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/services/apiClient.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<Response> {
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');
}
}
2 changes: 1 addition & 1 deletion frontend/tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -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"}
{"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"}
Loading