initial setup

This commit is contained in:
Jurgis Sakalauskas
2025-10-11 17:21:39 +03:00
commit ad2cf25bfd
37 changed files with 7084 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import { apiFetch } from './http-client';
export type HealthResponse = {
status: string;
service: string;
};
export function getHealth() {
return apiFetch<HealthResponse>('/health');
}
+25
View File
@@ -0,0 +1,25 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api';
type ApiFetchOptions = Omit<RequestInit, 'body'> & {
body?: Record<string, unknown>;
};
export async function apiFetch<TResponse>(path: string, options: ApiFetchOptions = {}): Promise<TResponse> {
const { body, headers, ...rest } = options;
const init: RequestInit = {
headers: {
'Content-Type': 'application/json',
...headers
},
body: body ? JSON.stringify(body) : undefined,
...rest
};
const response = await fetch(`${API_BASE_URL}${path}`, init);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json() as Promise<TResponse>;
}
+7
View File
@@ -0,0 +1,7 @@
import AppRoutes from './routes/app-routes';
function App() {
return <AppRoutes />;
}
export default App;
+15
View File
@@ -0,0 +1,15 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #1f2933;
background-color: #f8fafc;
}
body {
@apply m-0;
}
}
+58
View File
@@ -0,0 +1,58 @@
import { NavLink, Outlet } from 'react-router-dom';
const navItems = [
{ label: 'Health Check', to: '/' },
// future nav items can be added here
];
function AppLayout() {
return (
<div className="flex min-h-screen bg-slate-50 text-slate-800">
<aside className="flex w-64 flex-col border-r border-slate-200 bg-white px-6 py-8 shadow-sm">
<div className="mb-8">
<span className="block text-sm font-semibold uppercase tracking-wide text-slate-400">
BestChoice
</span>
<h1 className="text-2xl font-semibold text-slate-900">Control Center</h1>
</div>
<nav className="flex flex-1 flex-col gap-2">
{navItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
end
className={({ isActive }) =>
[
'rounded-md px-3 py-2 text-sm font-medium transition',
isActive
? 'bg-blue-600 text-white shadow-sm'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
].join(' ')
}
>
{item.label}
</NavLink>
))}
</nav>
<footer className="mt-8 text-xs text-slate-400">
<p>&copy; {new Date().getFullYear()} BestChoice</p>
</footer>
</aside>
<div className="flex flex-1 flex-col">
<header className="border-b border-slate-200 bg-white px-8 py-6 shadow-sm">
<h2 className="text-xl font-semibold text-slate-900">Dashboard</h2>
<p className="text-sm text-slate-500">Monitor service health and status</p>
</header>
<main className="flex-1 overflow-y-auto px-8 py-10">
<Outlet />
</main>
</div>
</div>
);
}
export default AppLayout;
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './app';
import './index.css';
import AppProviders from './providers/app-providers';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Root element not found');
}
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<AppProviders>
<App />
</AppProviders>
</React.StrictMode>
);
+39
View File
@@ -0,0 +1,39 @@
import { useHealthQuery } from '../query/use-health-query';
function HealthPage() {
const appName = import.meta.env.VITE_APP_NAME ?? 'BestChoice';
const { data, isLoading, isError, error, refetch, isFetching } = useHealthQuery();
const statusText = (() => {
if (isLoading) {
return 'loading...';
}
if (isError) {
return 'unreachable';
}
return data?.status ?? 'unknown';
})();
return (
<section className="mx-auto flex max-w-md flex-col items-center gap-4 px-4 py-12 text-center text-slate-800">
<h1 className="text-4xl font-semibold">{appName}</h1>
<p className="text-lg">
API status: <span className="font-medium text-blue-600">{statusText}</span>
</p>
<p className="text-sm text-slate-500">Service: {data?.service ?? 'n/a'}</p>
<button
type="button"
onClick={() => refetch()}
disabled={isFetching}
className="rounded-full bg-blue-600 px-5 py-2 text-white transition enabled:hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-slate-400"
>
{isFetching ? 'Refreshing…' : 'Refresh'}
</button>
{isError ? <p className="text-sm text-red-600">{(error as Error).message}</p> : null}
</section>
);
}
export default HealthPage;
+14
View File
@@ -0,0 +1,14 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { PropsWithChildren } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { queryClient } from '../query/query-client';
function AppProviders({ children }: PropsWithChildren) {
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</BrowserRouter>
);
}
export default AppProviders;
+10
View File
@@ -0,0 +1,10 @@
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1
}
}
});
+14
View File
@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { getHealth, type HealthResponse } from '../api/health';
const healthQueryKeys = {
all: ['health'] as const
};
export function useHealthQuery() {
return useQuery<HealthResponse>({
queryKey: healthQueryKeys.all,
queryFn: getHealth,
staleTime: 30_000
});
}
+16
View File
@@ -0,0 +1,16 @@
import { Route, Routes } from 'react-router-dom';
import AppLayout from '../layouts/app-layout';
import HealthPage from '../pages/health-page';
function AppRoutes() {
return (
<Routes>
<Route element={<AppLayout />}>
<Route index element={<HealthPage />} />
<Route path="*" element={<HealthPage />} />
</Route>
</Routes>
);
}
export default AppRoutes;