# 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 (