Files
whatshooked/tooldoc/REACT_MANTINE_TANSTACK.md
Hein f9773bd07f
Some checks failed
CI / Test (1.23) (push) Failing after -22m46s
CI / Test (1.22) (push) Failing after -22m32s
CI / Build (push) Failing after -23m30s
CI / Lint (push) Failing after -23m12s
refactor(API): Relspect integration
2026-02-05 13:39:43 +02:00

13 KiB

React + Mantine + TanStack Start Integration Guide

Overview

For WhatsHooked's admin interface, we'll use:

  • React 19: Modern React with hooks and suspense
  • Mantine: Component library for UI
  • TanStack Start: Full-stack React framework with server-side rendering
  • Oranguru: Enhanced Mantine components for grids and forms

Project Structure

frontend/
├── app/
│   ├── routes/
│   │   ├── __root.tsx        # Root layout
│   │   ├── index.tsx          # Dashboard
│   │   ├── login.tsx          # Login page
│   │   ├── users/
│   │   │   ├── index.tsx      # User list
│   │   │   ├── new.tsx        # Create user
│   │   │   └── $id/
│   │   │       ├── index.tsx  # User details
│   │   │       └── edit.tsx   # Edit user
│   │   ├── hooks/
│   │   │   ├── index.tsx      # Hook list
│   │   │   └── ...
│   │   └── accounts/
│   │       ├── index.tsx      # WhatsApp accounts
│   │       └── ...
│   ├── components/
│   │   ├── Layout.tsx
│   │   ├── Navbar.tsx
│   │   ├── UserGrid.tsx
│   │   └── ...
│   ├── lib/
│   │   ├── api.ts             # API client
│   │   ├── auth.ts            # Auth utilities
│   │   └── types.ts           # TypeScript types
│   └── styles/
│       └── global.css
├── public/
│   └── assets/
├── package.json
├── tsconfig.json
└── vite.config.ts

Installation

npm create @tanstack/start@latest
cd whatshooked-admin
npm install @mantine/core @mantine/hooks @mantine/notifications @mantine/form @mantine/datatable
npm install @warkypublic/oranguru
npm install @tanstack/react-query axios
npm install -D @types/react @types/react-dom

Basic Setup

app/routes/__root.tsx

import { createRootRoute, Outlet } from '@tanstack/react-router';
import { MantineProvider } from '@mantine/core';
import { MantineBetterMenusProvider } from '@warkypublic/oranguru';
import { Notifications } from '@mantine/notifications';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export const Route = createRootRoute({
  component: () => (
    <QueryClientProvider client={queryClient}>
      <MantineProvider>
        <MantineBetterMenusProvider>
          <Notifications />
          <Outlet />
        </MantineBetterMenusProvider>
      </MantineProvider>
    </QueryClientProvider>
  ),
});

app/lib/api.ts

import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8825/api/v1',
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add auth token to requests
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Handle auth errors
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('auth_token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;

// API methods
export const authApi = {
  login: (username: string, password: string) =>
    api.post('/auth/login', { username, password }),
  
  logout: () =>
    api.post('/auth/logout'),
  
  getProfile: () =>
    api.get('/auth/profile'),
};

export const usersApi = {
  list: (params?: any) =>
    api.get('/users', { params }),
  
  get: (id: string) =>
    api.get(`/users/${id}`),
  
  create: (data: any) =>
    api.post('/users', data),
  
  update: (id: string, data: any) =>
    api.put(`/users/${id}`, data),
  
  delete: (id: string) =>
    api.delete(`/users/${id}`),
};

export const hooksApi = {
  list: () =>
    api.get('/hooks'),
  
  create: (data: any) =>
    api.post('/hooks', data),
  
  update: (id: string, data: any) =>
    api.put(`/hooks/${id}`, data),
  
  delete: (id: string) =>
    api.delete(`/hooks/${id}`),
};

export const accountsApi = {
  list: () =>
    api.get('/accounts'),
  
  pair: (accountId: string) =>
    api.post(`/accounts/${accountId}/pair`),
  
  disconnect: (accountId: string) =>
    api.post(`/accounts/${accountId}/disconnect`),
  
  getQRCode: (accountId: string) =>
    api.get(`/accounts/${accountId}/qr`, { responseType: 'blob' }),
};

Authentication

app/routes/login.tsx

import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useForm } from '@mantine/form';
import { TextInput, PasswordInput, Button, Paper, Title, Container } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { authApi } from '../lib/api';

export const Route = createFileRoute('/login')({
  component: LoginPage,
});

function LoginPage() {
  const navigate = useNavigate();
  
  const form = useForm({
    initialValues: {
      username: '',
      password: '',
    },
    validate: {
      username: (value) => (value.length < 3 ? 'Username too short' : null),
      password: (value) => (value.length < 6 ? 'Password too short' : null),
    },
  });

  const handleSubmit = async (values: typeof form.values) => {
    try {
      const response = await authApi.login(values.username, values.password);
      localStorage.setItem('auth_token', response.data.token);
      notifications.show({
        title: 'Success',
        message: 'Logged in successfully',
        color: 'green',
      });
      navigate({ to: '/' });
    } catch (error) {
      notifications.show({
        title: 'Error',
        message: 'Invalid credentials',
        color: 'red',
      });
    }
  };

  return (
    <Container size={420} my={40}>
      <Title ta="center">WhatsHooked Admin</Title>
      <Paper withBorder shadow="md" p={30} mt={30} radius="md">
        <form onSubmit={form.onSubmit(handleSubmit)}>
          <TextInput
            label="Username"
            placeholder="admin"
            required
            {...form.getInputProps('username')}
          />
          <PasswordInput
            label="Password"
            placeholder="Your password"
            required
            mt="md"
            {...form.getInputProps('password')}
          />
          <Button fullWidth mt="xl" type="submit">
            Sign in
          </Button>
        </form>
      </Paper>
    </Container>
  );
}

Data Grid with Oranguru

app/routes/users/index.tsx

import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { DataTable } from '@mantine/datatable';
import { useMantineBetterMenus } from '@warkypublic/oranguru';
import { Button, Group, Text } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { usersApi } from '../../lib/api';

export const Route = createFileRoute('/users/')({
  component: UsersPage,
});

function UsersPage() {
  const { show } = useMantineBetterMenus();
  
  const { data: users, isLoading, refetch } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await usersApi.list();
      return response.data;
    },
  });

  const handleContextMenu = (e: React.MouseEvent, user: any) => {
    e.preventDefault();
    show('user-menu', {
      x: e.clientX,
      y: e.clientY,
      items: [
        {
          label: 'Edit',
          onClick: () => navigate({ to: `/users/${user.id}/edit` }),
        },
        {
          label: 'View Details',
          onClick: () => navigate({ to: `/users/${user.id}` }),
        },
        {
          isDivider: true,
        },
        {
          label: 'Delete',
          onClickAsync: async () => {
            await usersApi.delete(user.id);
            notifications.show({
              message: 'User deleted successfully',
              color: 'green',
            });
            refetch();
          },
        },
      ],
    });
  };

  const columns = [
    { accessor: 'name', title: 'Name' },
    { accessor: 'email', title: 'Email' },
    { accessor: 'role', title: 'Role' },
    {
      accessor: 'actions',
      title: '',
      render: (user: any) => (
        <Group gap="xs">
          <Button size="xs" onClick={() => navigate({ to: `/users/${user.id}/edit` })}>
            Edit
          </Button>
        </Group>
      ),
    },
  ];

  return (
    <div>
      <Group justify="space-between" mb="md">
        <Text size="xl" fw={700}>Users</Text>
        <Button onClick={() => navigate({ to: '/users/new' })}>
          Create User
        </Button>
      </Group>
      
      <DataTable
        columns={columns}
        records={users || []}
        fetching={isLoading}
        onRowContextMenu={({ event, record }) => handleContextMenu(event, record)}
      />
    </div>
  );
}

Forms

app/routes/users/new.tsx

import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useForm } from '@mantine/form';
import { TextInput, Select, Button, Paper } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { usersApi } from '../../lib/api';

export const Route = createFileRoute('/users/new')({
  component: NewUserPage,
});

function NewUserPage() {
  const navigate = useNavigate();
  
  const form = useForm({
    initialValues: {
      name: '',
      email: '',
      password: '',
      role: 'user',
    },
    validate: {
      name: (value) => (value.length < 2 ? 'Name too short' : null),
      email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
      password: (value) => (value.length < 6 ? 'Password too short' : null),
    },
  });

  const handleSubmit = async (values: typeof form.values) => {
    try {
      await usersApi.create(values);
      notifications.show({
        message: 'User created successfully',
        color: 'green',
      });
      navigate({ to: '/users' });
    } catch (error) {
      notifications.show({
        message: 'Failed to create user',
        color: 'red',
      });
    }
  };

  return (
    <Paper p="md">
      <form onSubmit={form.onSubmit(handleSubmit)}>
        <TextInput
          label="Name"
          placeholder="John Doe"
          required
          {...form.getInputProps('name')}
        />
        <TextInput
          label="Email"
          placeholder="john@example.com"
          required
          mt="md"
          {...form.getInputProps('email')}
        />
        <PasswordInput
          label="Password"
          placeholder="Password"
          required
          mt="md"
          {...form.getInputProps('password')}
        />
        <Select
          label="Role"
          data={['admin', 'user', 'viewer']}
          required
          mt="md"
          {...form.getInputProps('role')}
        />
        <Group justify="flex-end" mt="xl">
          <Button variant="subtle" onClick={() => navigate({ to: '/users' })}>
            Cancel
          </Button>
          <Button type="submit">Create</Button>
        </Group>
      </form>
    </Paper>
  );
}

Layout with Navigation

app/components/Layout.tsx

import { AppShell, NavLink, Group, Title } from '@mantine/core';
import { IconUsers, IconWebhook, IconBrandWhatsapp, IconKey } from '@tabler/icons-react';
import { Link, useLocation } from '@tanstack/react-router';

export function Layout({ children }: { children: React.ReactNode }) {
  const location = useLocation();
  
  const navItems = [
    { icon: IconUsers, label: 'Users', to: '/users' },
    { icon: IconWebhook, label: 'Hooks', to: '/hooks' },
    { icon: IconBrandWhatsapp, label: 'WhatsApp Accounts', to: '/accounts' },
    { icon: IconKey, label: 'API Keys', to: '/api-keys' },
  ];

  return (
    <AppShell
      navbar={{ width: 250, breakpoint: 'sm' }}
      padding="md"
    >
      <AppShell.Navbar p="md">
        <Title order={3} mb="md">WhatsHooked</Title>
        {navItems.map((item) => (
          <NavLink
            key={item.to}
            component={Link}
            to={item.to}
            label={item.label}
            leftSection={<item.icon size={20} />}
            active={location.pathname.startsWith(item.to)}
          />
        ))}
      </AppShell.Navbar>
      
      <AppShell.Main>{children}</AppShell.Main>
    </AppShell>
  );
}

Best Practices

  1. Code Splitting: Use lazy loading for routes
  2. Error Boundaries: Wrap components in error boundaries
  3. Loading States: Show loading indicators with Suspense
  4. Optimistic Updates: Update UI before API response
  5. Form Validation: Use Mantine form with validation
  6. Type Safety: Use TypeScript for all API calls
  7. Query Invalidation: Refetch data after mutations
  8. Auth Protection: Protect routes with auth guards

References