refactor(API): ✨ Relspect integration
This commit is contained in:
504
tooldoc/REACT_MANTINE_TANSTACK.md
Normal file
504
tooldoc/REACT_MANTINE_TANSTACK.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# 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: () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider>
|
||||
<MantineBetterMenusProvider>
|
||||
<Notifications />
|
||||
<Outlet />
|
||||
</MantineBetterMenusProvider>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<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
|
||||
|
||||
```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
|
||||
|
||||
```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
|
||||
|
||||
```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
|
||||
|
||||
- TanStack Start: https://tanstack.com/start
|
||||
- Mantine: https://mantine.dev
|
||||
- TanStack Query: https://tanstack.com/query
|
||||
- Oranguru: See ORANGURU.md
|
||||
Reference in New Issue
Block a user