Initial commit

This commit is contained in:
allagroup
2026-04-17 07:21:17 +00:00
commit fe38508201
470 changed files with 74105 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
# Mock API Guide
## Table of Contents
1. [Structure](#structure)
2. [Full Template](#full-template)
3. [Key Patterns](#key-patterns)
4. [Integrating with React Query](#integrating-with-react-query)
---
## Structure
Each mock API file lives in `src/constants/mock-api-<name>.ts` and is a self-contained in-memory database. It uses:
- **faker** for generating sample data
- **match-sorter** for fuzzy search across fields
- **delay** (from `./mock-api`) to simulate network latency
The `delay` function is exported from `src/constants/mock-api.ts`:
```tsx
export async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
```
---
## Full Template
```tsx
import { faker } from '@faker-js/faker';
import { matchSorter } from 'match-sorter';
import { delay } from './mock-api';
// 1. Define the entity type
export type Order = {
id: number;
customer: string;
email: string;
status: string;
total: number;
created_at: string;
updated_at: string;
};
// 2. Create the fake database object
export const fakeOrders = {
records: [] as Order[],
// 3. Initialize with faker data
initialize() {
const statuses = ['pending', 'processing', 'completed', 'cancelled'];
for (let i = 1; i <= 20; i++) {
this.records.push({
id: i,
customer: faker.person.fullName(),
email: faker.internet.email(),
status: faker.helpers.arrayElement(statuses),
total: parseFloat(faker.commerce.price({ min: 10, max: 500 })),
created_at: faker.date.between({ from: '2023-01-01', to: Date.now() }).toISOString(),
updated_at: faker.date.recent().toISOString()
});
}
},
// 4. Get all with optional search (used internally)
async getAll({ search }: { search?: string } = {}) {
let items = [...this.records];
if (search) {
items = matchSorter(items, search, {
keys: ['customer', 'email']
});
}
return items;
},
// 5. Paginated list with filtering and sorting
async getOrders(params: {
page?: number;
limit?: number;
search?: string;
statuses?: string;
sort?: string;
}) {
await delay(800);
const { page = 1, limit = 10, search, statuses, sort } = params;
let items = await this.getAll({ search });
// Filter by comma-separated values
if (statuses) {
const statusList = statuses.split('.');
items = items.filter((item) => statusList.includes(item.status));
}
// Sort by column
if (sort) {
const parsedSort = JSON.parse(sort) as { id: string; desc: boolean }[];
if (parsedSort.length > 0) {
const { id, desc } = parsedSort[0];
items.sort((a, b) => {
const aVal = a[id as keyof Order];
const bVal = b[id as keyof Order];
if (aVal < bVal) return desc ? 1 : -1;
if (aVal > bVal) return desc ? -1 : 1;
return 0;
});
}
}
// Paginate
const total_items = items.length;
items = items.slice((page - 1) * limit, page * limit);
return { items, total_items };
},
// 6. Get single record by ID
async getOrderById(id: number) {
await delay(800);
return this.records.find((r) => r.id === id) || null;
},
// 7. Create
async createOrder(data: Omit<Order, 'id' | 'created_at' | 'updated_at'>) {
await delay(800);
const newRecord: Order = {
...data,
id: this.records.length + 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
this.records.push(newRecord);
return newRecord;
},
// 8. Update
async updateOrder(id: number, data: Partial<Order>) {
await delay(800);
const idx = this.records.findIndex((r) => r.id === id);
if (idx === -1) return null;
this.records[idx] = {
...this.records[idx],
...data,
updated_at: new Date().toISOString()
};
return this.records[idx];
},
// 9. Delete
async deleteOrder(id: number) {
await delay(800);
this.records = this.records.filter((r) => r.id !== id);
return true;
}
};
// 10. Auto-initialize on import
fakeOrders.initialize();
```
---
## Key Patterns
### Search with match-sorter
Always specify which fields to search across:
```tsx
matchSorter(items, search, { keys: ['customer', 'email', 'status'] });
```
### Comma-separated filter values
For multi-select filters (roles, statuses), the URL param uses `.` as delimiter:
```tsx
if (statuses) {
const list = statuses.split('.');
items = items.filter((item) => list.includes(item.status));
}
```
### Computed column sorting
When a table has a computed column (e.g., combining first_name + last_name into "name"), handle it in the sort logic:
```tsx
if (id === 'name') {
const aName = `${a.first_name} ${a.last_name}`;
const bName = `${b.first_name} ${b.last_name}`;
return desc ? bName.localeCompare(aName) : aName.localeCompare(bName);
}
```
### Return shape
List methods must return `{ items, total_items }` (or `{ products, total }` etc. — match the query option expectations). The total is the count **before** pagination, used for `pageCount` calculation.
---
## Integrating with the API Layer
The mock API is only imported in `service.ts`. Queries and components import from the service and types files:
```
mock-api-orders.ts → api/service.ts → api/queries.ts → components
(data source) (data access) (key factory + (useSuspenseQuery
queryOptions) + useMutation)
```
**service.ts** imports from the mock API:
```tsx
import { fakeOrders } from '@/constants/mock-api-orders';
import type { OrderFilters, OrdersResponse } from './types';
export async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {
return fakeOrders.getOrders(filters);
}
export async function createOrder(data: OrderMutationPayload) {
return fakeOrders.createOrder(data);
}
```
**queries.ts** imports from service, uses key factories:
```tsx
import { getOrders } from './service';
import type { OrderFilters } from './types';
export const orderKeys = {
all: ['orders'] as const,
list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,
detail: (id: number) => [...orderKeys.all, 'detail', id] as const
};
export const ordersQueryOptions = (filters: OrderFilters) =>
queryOptions({ queryKey: orderKeys.list(filters), queryFn: () => getOrders(filters) });
```
**Mutations** in components use service functions + key factories:
```tsx
import { createOrder } from '../api/service';
import { orderKeys } from '../api/queries';
const mutation = useMutation({
mutationFn: (data) => createOrder(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: orderKeys.all })
});
```