# 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
```bash
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
```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: () => (
),
});
```
### app/lib/api.ts
```typescript
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
```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 (
WhatsHooked Admin
);
}
```
## Data Grid with Oranguru
### app/routes/users/index.tsx
```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) => (
),
},
];
return (
Users
handleContextMenu(event, record)}
/>
);
}
```
## Forms
### app/routes/users/new.tsx
```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 (
);
}
```
## Layout with Navigation
### app/components/Layout.tsx
```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 (
WhatsHooked
{navItems.map((item) => (
}
active={location.pathname.startsWith(item.to)}
/>
))}
{children}
);
}
```
## 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
- TanStack Start: https://tanstack.com/start
- Mantine: https://mantine.dev
- TanStack Query: https://tanstack.com/query
- Oranguru: See ORANGURU.md