1. Project setup#

Prerequisites#

  • Node.js 18+
  • Backend from the backend tutorial running on http://localhost:8080 with context path /api

Scaffold#

npm create vite@latest your-web -- --template react-ts
cd your-web

Dependencies#

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/vite

Vite 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.