mirror of
https://github.com/sakaljurgis/best-choice.git
synced 2026-07-08 21:47:40 +00:00
initial setup
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BestChoice</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@best-choice/client",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"test": "echo \"No tests yet\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"description": "React frontend for the best-choice project",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
# Public Assets
|
||||
|
||||
Place static files (images, documents, etc.) in this directory to be served by Vite during development and included in the production build.
|
||||
@@ -0,0 +1,10 @@
|
||||
import { apiFetch } from './http-client';
|
||||
|
||||
export type HealthResponse = {
|
||||
status: string;
|
||||
service: string;
|
||||
};
|
||||
|
||||
export function getHealth() {
|
||||
return apiFetch<HealthResponse>('/health');
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import AppRoutes from './routes/app-routes';
|
||||
|
||||
function App() {
|
||||
return <AppRoutes />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>© {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;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vite/client"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'node:path';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const env = loadEnv(mode, rootDir, '');
|
||||
const clientPort = Number(env.CLIENT_PORT) || 5173;
|
||||
const proxyTarget = env.API_PROXY_TARGET || 'http://localhost:3000';
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: clientPort,
|
||||
proxy: {
|
||||
'/api': proxyTarget
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user