1. Project setup#
What this step does. Scaffolds a plain Vite + React + TypeScript app, adds Mantine and the three Palmyra frontend libraries, configures a dev-server proxy so
/apilands on the SpringBoot backend, and sets up routing.
npm create vite@latest empmgmt-web -- --template react-ts
cd empmgmt-webInstall#
npm install react@19 react-dom@19 \
@mantine/core@8 @mantine/hooks@8 @mantine/dates@8 \
react-router-dom@7 react-toastify dayjs \
@palmyralabs/palmyra-wire \
@palmyralabs/rt-forms \
@palmyralabs/rt-forms-mantine \
@palmyralabs/template-tribble@palmyralabs/template-tribble supplies SummaryGrid, DialogNewForm, and DialogEditForm — prebuilt templates that cut the repetitive grid / modal plumbing out of each page. This is the same pattern the thinxar/clinic reference uses.
Vite proxy#
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 3000, proxy: { '/api': 'http://localhost:8080' } }
});App bootstrap#
// src/main.tsx
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import 'react-toastify/dist/ReactToastify.css';
import { createRoot } from 'react-dom/client';
import { MantineProvider } from '@mantine/core';
import { ToastContainer } from 'react-toastify';
import { StoreFactoryContext } from '@palmyralabs/palmyra-wire';
import AppStoreFactory from './wire/StoreFactory';
import App from './App';
createRoot(document.getElementById('root')!).render(
<MantineProvider>
<StoreFactoryContext.Provider value={AppStoreFactory}>
<App />
<ToastContainer position="top-right" autoClose={3000} />
</StoreFactoryContext.Provider>
</MantineProvider>
);Routing shell#
Each entity gets a list route and a view route — matching the clinic convention where the grid row click navigates into a detail / edit page:
// src/App.tsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import DepartmentGridPage from './pages/department/DepartmentGridPage';
import DepartmentViewPage from './pages/department/view/DepartmentViewPage';
import EmployeeGridPage from './pages/employee/EmployeeGridPage';
import EmployeeViewPage from './pages/employee/view/EmployeeViewPage';
export default function App() {
return (
<BrowserRouter>
<nav style={{ padding: 16, display: 'flex', gap: 16 }}>
<Link to="/departments">Departments</Link>
<Link to="/employees">Employees</Link>
</nav>
<Routes>
<Route path="/departments" element={<DepartmentGridPage />} />
<Route path="/departments/view/:id" element={<DepartmentViewPage />} />
<Route path="/employees" element={<EmployeeGridPage />} />
<Route path="/employees/view/:id" element={<EmployeeViewPage />} />
</Routes>
</BrowserRouter>
);
}Folder layout per entity#
Following the clinic’s masterdata pattern — one folder per entity, with a view/ subfolder holding the detail page and its edit dialog:
src/pages/
department/
DepartmentGridPage.tsx ← master list + "new" modal
DepartmentNewForm.tsx ← DialogNewForm
view/
DepartmentViewPage.tsx ← detail page shell
DepartmentViewForm.tsx ← read-only view form (uses PalmyraViewForm)
DepartmentEditForm.tsx ← DialogEditForm (reuses the new-form field set)
employee/
EmployeeGridPage.tsx
EmployeeNewForm.tsx
view/
EmployeeViewPage.tsx
EmployeeViewForm.tsx
EmployeeEditForm.tsxRun npm run dev and confirm the page loads on http://localhost:3000. The next step wires the store factory so those page components can actually talk to the backend.