1. Project setup#
Prerequisites#
- Node.js 18+
- Backend from the backend tutorial running on
http://localhost:8080with context path/api
Scaffold#
npm create vite@latest your-web -- --template react-ts
cd your-webDependencies#
The clinic sample composes the stack out of React 19 + Mantine 8 + the Palmyra UI libraries + a handful of utilities. Minimum viable install:
# React + Mantine
npm install react@19 react-dom@19 @mantine/core@8 @mantine/hooks@8 @mantine/dates@8
# Routing + transport + toasts
npm install react-router-dom@7 axios react-toastify lucide-react
# Palmyra frontend libraries
npm install @palmyralabs/palmyra-wire @palmyralabs/rt-forms \
@palmyralabs/rt-forms-mantine @palmyralabs/template-tribble
# Dev
npm install -D vite @vitejs/plugin-react typescript vite-tsconfig-paths \
tailwindcss @tailwindcss/viteVite config#
Proxy /api to the backend so the Palmyra store can use relative paths.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [tsconfigPaths(), react(), tailwindcss()],
server: {
port: 3000,
host: '0.0.0.0',
open: true,
proxy: { '/api': 'http://localhost:8080/' }
}
});App entrypoint#
Mantine styles must be imported before your own CSS. StoreFactoryContext from palmyra-wire makes the shared store factory available everywhere without prop-drilling.
// src/main.tsx
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import './index.css';
import { createRoot } from 'react-dom/client';
import { MantineProvider } from '@mantine/core';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { StoreFactoryContext } from '@palmyralabs/palmyra-wire';
import App from './App';
import AppStoreFactory from './wire/StoreFactory';
createRoot(document.getElementById('root')!).render(
<MantineProvider>
<StoreFactoryContext.Provider value={AppStoreFactory}>
<App />
<ToastContainer position="top-right" autoClose={3000} />
</StoreFactoryContext.Provider>
</MantineProvider>
);The clinic sample omits Mantine’s ColorSchemeScript and @mantine/notifications — dark mode is a local ThemeContext that toggles a class on <html>, and toasts come from react-toastify. Add them back if you prefer the Mantine-native approach.
Routing shell#
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoginPage from './pages/auth/LoginPage';
import MainLayout from './layout/MainLayout';
import { generateRoutes } from './routes';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/app/*" element={<MainLayout />}>
{generateRoutes()}
</Route>
</Routes>
</BrowserRouter>
);
}generateRoutes() walks a route-tree config and emits <Route> entries, mirroring the sample’s src/routes/appRoutesX.tsx pattern. This keeps the sidebar menu and the route list in sync — both are projected from the same tree.