mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
712 lines
16 KiB
Markdown
712 lines
16 KiB
Markdown
# Frontend Utility Hooks & Components Guide
|
|
|
|
Complete TypeScript/TSX utilities that mirror the backend helpers and make frontend development much easier.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Hooks](#hooks)
|
|
- [usePaginatedData](#usepaginateddata)
|
|
- [useApiMutation](#useapimutation)
|
|
- [useFormValidation](#useformvalidation)
|
|
- [useQueryBuilder](#usequerybuilder)
|
|
- [useToast](#usetoast)
|
|
- [useBatchSelection](#usebatchselection)
|
|
2. [Components](#components)
|
|
- [DataTable](#datatable)
|
|
- [ToastContainer](#toastcontainer)
|
|
3. [Utilities](#utilities)
|
|
- [Export Functions](#export-functions)
|
|
4. [Complete Example](#complete-example)
|
|
|
|
---
|
|
|
|
## Hooks
|
|
|
|
### usePaginatedData
|
|
|
|
**Purpose:** Fetch paginated data with search, sort, and filters in one line.
|
|
|
|
**Features:**
|
|
- Automatic pagination management
|
|
- Search functionality
|
|
- Sorting
|
|
- Filtering
|
|
- Loading and error states
|
|
- Works seamlessly with backend QueryParser
|
|
|
|
```tsx
|
|
import { usePaginatedData } from '../hooks/usePaginatedData';
|
|
|
|
interface Article {
|
|
id: number;
|
|
title: string;
|
|
published: boolean;
|
|
}
|
|
|
|
function ArticleList() {
|
|
const {
|
|
data: articles,
|
|
meta,
|
|
loading,
|
|
error,
|
|
setPage,
|
|
setSearch,
|
|
setSort,
|
|
setFilters,
|
|
refresh,
|
|
} = usePaginatedData<Article>('/articles', {
|
|
page_size: 20,
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<input
|
|
type="text"
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Search..."
|
|
/>
|
|
|
|
<select onChange={(e) => setFilters({ published: e.target.value })}>
|
|
<option value="">All</option>
|
|
<option value="true">Published</option>
|
|
<option value="false">Draft</option>
|
|
</select>
|
|
|
|
{loading && <p>Loading...</p>}
|
|
{error && <p>Error: {error}</p>}
|
|
|
|
<ul>
|
|
{articles.map(article => (
|
|
<li key={article.id}>{article.title}</li>
|
|
))}
|
|
</ul>
|
|
|
|
{meta && (
|
|
<button
|
|
onClick={() => setPage(meta.page + 1)}
|
|
disabled={!meta.has_next}
|
|
>
|
|
Next Page
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### useApiMutation
|
|
|
|
**Purpose:** Handle POST, PUT, PATCH, DELETE requests with loading states.
|
|
|
|
**Features:**
|
|
- Automatic loading states
|
|
- Error handling
|
|
- Success tracking
|
|
- TypeScript support
|
|
|
|
```tsx
|
|
import { useApiPost, useApiDelete } from '../hooks/useApiMutation';
|
|
|
|
function ArticleForm() {
|
|
// Create article
|
|
const { mutate: createArticle, loading, error, success } = useApiPost<Article>('/articles');
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const article = await createArticle({
|
|
title: 'New Article',
|
|
content: 'Content here...',
|
|
});
|
|
|
|
if (article) {
|
|
console.log('Created:', article);
|
|
}
|
|
};
|
|
|
|
// Delete article
|
|
const deleteArticle = useApiDelete((data: { id: number }) => `/articles/${data.id}`);
|
|
|
|
const handleDelete = async (id: number) => {
|
|
await deleteArticle.mutate({ id });
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit}>
|
|
<button type="submit" disabled={loading}>
|
|
{loading ? 'Creating...' : 'Create Article'}
|
|
</button>
|
|
{error && <p className="error">{error}</p>}
|
|
{success && <p className="success">Article created!</p>}
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
### useFormValidation
|
|
|
|
**Purpose:** Form validation with built-in rules and error messages.
|
|
|
|
**Features:**
|
|
- Required, min/max length, pattern, email, URL validators
|
|
- Custom validators
|
|
- Automatic error messages
|
|
- Touch tracking
|
|
- Easy integration with forms
|
|
|
|
```tsx
|
|
import { useFormValidation } from '../hooks/useFormValidation';
|
|
|
|
interface ArticleForm {
|
|
title: string;
|
|
content: string;
|
|
email: string;
|
|
url: string;
|
|
}
|
|
|
|
function CreateArticleForm() {
|
|
const {
|
|
values,
|
|
errors,
|
|
touched,
|
|
handleChange,
|
|
handleBlur,
|
|
handleSubmit,
|
|
} = useFormValidation<ArticleForm>(
|
|
{
|
|
title: '',
|
|
content: '',
|
|
email: '',
|
|
url: '',
|
|
},
|
|
{
|
|
title: { required: true, min: 3, max: 200 },
|
|
content: { required: true, min: 10 },
|
|
email: { required: true, email: true },
|
|
url: { url: true },
|
|
}
|
|
);
|
|
|
|
const onSubmit = async (data: ArticleForm) => {
|
|
console.log('Valid data:', data);
|
|
// Call API here
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
<div>
|
|
<input
|
|
type="text"
|
|
name="title"
|
|
value={values.title}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
placeholder="Title"
|
|
/>
|
|
{touched.title && errors.title && (
|
|
<span className="error">{errors.title}</span>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<textarea
|
|
name="content"
|
|
value={values.content}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
placeholder="Content"
|
|
/>
|
|
{touched.content && errors.content && (
|
|
<span className="error">{errors.content}</span>
|
|
)}
|
|
</div>
|
|
|
|
<button type="submit">Create Article</button>
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
### useQueryBuilder
|
|
|
|
**Purpose:** Build query strings for API calls with filters, search, and sort.
|
|
|
|
**Features:**
|
|
- Filter management
|
|
- Search management
|
|
- Sort management
|
|
- Pagination
|
|
- URL-friendly query string generation
|
|
|
|
```tsx
|
|
import { useQueryBuilder } from '../hooks/useQueryBuilder';
|
|
|
|
function ArticleFilters() {
|
|
const query = useQueryBuilder(20);
|
|
|
|
return (
|
|
<div>
|
|
<input
|
|
type="text"
|
|
placeholder="Search..."
|
|
onChange={(e) => query.setSearch(e.target.value)}
|
|
/>
|
|
|
|
<select onChange={(e) => query.setFilter('published', e.target.value)}>
|
|
<option value="">All</option>
|
|
<option value="true">Published</option>
|
|
<option value="false">Draft</option>
|
|
</select>
|
|
|
|
<button onClick={() => query.setSort('created_at', 'desc')}>
|
|
Sort by Date
|
|
</button>
|
|
|
|
<p>Query: {query.queryString}</p>
|
|
{/* Use in API call: `/articles?${query.queryString}` */}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### useToast
|
|
|
|
**Purpose:** Display toast notifications for user feedback.
|
|
|
|
**Features:**
|
|
- Success, error, warning, info types
|
|
- Auto-dismiss
|
|
- Manual dismiss
|
|
- Queue management
|
|
|
|
```tsx
|
|
import { useToast } from '../hooks/useToast';
|
|
import { ToastContainer } from '../components/common/ToastContainer';
|
|
|
|
function App() {
|
|
const toast = useToast();
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
// Save logic
|
|
toast.success('Article saved successfully!');
|
|
} catch (error) {
|
|
toast.error('Failed to save article');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />
|
|
|
|
<button onClick={handleSave}>Save</button>
|
|
<button onClick={() => toast.info('This is info')}>Show Info</button>
|
|
<button onClick={() => toast.warning('Warning!')}>Show Warning</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### useBatchSelection
|
|
|
|
**Purpose:** Manage selection of multiple items in tables/lists.
|
|
|
|
**Features:**
|
|
- Select/deselect individual items
|
|
- Select all / deselect all
|
|
- Track selected items
|
|
- Get selected IDs or full items
|
|
|
|
```tsx
|
|
import { useBatchSelection } from '../hooks/useBatchSelection';
|
|
|
|
function ArticleTable() {
|
|
const articles = [
|
|
{ id: 1, title: 'Article 1' },
|
|
{ id: 2, title: 'Article 2' },
|
|
{ id: 3, title: 'Article 3' },
|
|
];
|
|
|
|
const selection = useBatchSelection(articles, 'id');
|
|
|
|
const handleBatchDelete = () => {
|
|
const ids = selection.getSelectedIds();
|
|
console.log('Delete articles:', ids);
|
|
// Call batch delete API
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div>
|
|
<input
|
|
type="checkbox"
|
|
checked={selection.isAllSelected}
|
|
onChange={selection.toggleAll}
|
|
/>
|
|
<label>Select All</label>
|
|
</div>
|
|
|
|
{articles.map((article) => (
|
|
<div key={article.id}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selection.isSelected(article.id)}
|
|
onChange={() => selection.toggle(article.id)}
|
|
/>
|
|
<span>{article.title}</span>
|
|
</div>
|
|
))}
|
|
|
|
{selection.selectedIds.size > 0 && (
|
|
<button onClick={handleBatchDelete}>
|
|
Delete {selection.selectedIds.size} selected
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Components
|
|
|
|
### DataTable
|
|
|
|
**Purpose:** Feature-rich data table with sorting, selection, and custom rendering.
|
|
|
|
**Features:**
|
|
- Sortable columns
|
|
- Row selection (single or multiple)
|
|
- Custom cell rendering
|
|
- Actions column
|
|
- Loading and empty states
|
|
- Responsive design
|
|
|
|
```tsx
|
|
import { DataTable, Column } from '../components/common/DataTable';
|
|
|
|
interface Article {
|
|
id: number;
|
|
title: string;
|
|
published: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
function ArticleTable() {
|
|
const articles: Article[] = [/* ... */];
|
|
const selection = useBatchSelection(articles, 'id');
|
|
|
|
const columns: Column<Article>[] = [
|
|
{
|
|
key: 'id',
|
|
label: 'ID',
|
|
sortable: true,
|
|
width: '80px',
|
|
},
|
|
{
|
|
key: 'title',
|
|
label: 'Title',
|
|
sortable: true,
|
|
render: (article) => (
|
|
<strong>{article.title}</strong>
|
|
),
|
|
},
|
|
{
|
|
key: 'published',
|
|
label: 'Status',
|
|
render: (article) => (
|
|
<span className={article.published ? 'published' : 'draft'}>
|
|
{article.published ? 'Published' : 'Draft'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'created_at',
|
|
label: 'Created',
|
|
sortable: true,
|
|
render: (article) => new Date(article.created_at).toLocaleDateString(),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<DataTable
|
|
data={articles}
|
|
columns={columns}
|
|
selectable
|
|
selectedIds={selection.selectedIds}
|
|
onToggleSelect={selection.toggle}
|
|
onToggleSelectAll={selection.toggleAll}
|
|
isAllSelected={selection.isAllSelected}
|
|
isSomeSelected={selection.isSomeSelected}
|
|
onSort={(field) => console.log('Sort by:', field)}
|
|
actions={(article) => (
|
|
<div>
|
|
<button onClick={() => console.log('Edit', article.id)}>Edit</button>
|
|
<button onClick={() => console.log('Delete', article.id)}>Delete</button>
|
|
</div>
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
### ToastContainer
|
|
|
|
**Purpose:** Display toast notifications from useToast hook.
|
|
|
|
```tsx
|
|
import { useToast } from '../hooks/useToast';
|
|
import { ToastContainer } from '../components/common/ToastContainer';
|
|
|
|
function App() {
|
|
const toast = useToast();
|
|
|
|
return (
|
|
<div>
|
|
<ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />
|
|
{/* Your app content */}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Utilities
|
|
|
|
### Export Functions
|
|
|
|
**Purpose:** Export data to CSV/JSON files.
|
|
|
|
```tsx
|
|
import {
|
|
exportToCSV,
|
|
exportToJSON,
|
|
exportFromAPI,
|
|
getExportFilename,
|
|
copyToClipboard,
|
|
} from '../utils/export';
|
|
|
|
function ExportButtons() {
|
|
const articles = [
|
|
{ id: 1, title: 'Article 1', published: true },
|
|
{ id: 2, title: 'Article 2', published: false },
|
|
];
|
|
|
|
// Export to CSV
|
|
const handleExportCSV = () => {
|
|
const filename = getExportFilename('articles', 'csv');
|
|
exportToCSV(articles, filename);
|
|
};
|
|
|
|
// Export to JSON
|
|
const handleExportJSON = () => {
|
|
const filename = getExportFilename('articles', 'json');
|
|
exportToJSON(articles, filename);
|
|
};
|
|
|
|
// Export from API endpoint
|
|
const handleExportFromAPI = async () => {
|
|
try {
|
|
await exportFromAPI('/articles/export', 'articles.csv', 'csv');
|
|
} catch (error) {
|
|
console.error('Export failed');
|
|
}
|
|
};
|
|
|
|
// Copy to clipboard
|
|
const handleCopy = async () => {
|
|
const success = await copyToClipboard(JSON.stringify(articles, null, 2));
|
|
if (success) {
|
|
alert('Copied to clipboard!');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<button onClick={handleExportCSV}>Export CSV</button>
|
|
<button onClick={handleExportJSON}>Export JSON</button>
|
|
<button onClick={handleExportFromAPI}>Export from API</button>
|
|
<button onClick={handleCopy}>Copy to Clipboard</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Complete Example
|
|
|
|
See `ArticleListExample.tsx` for a complete implementation using all utilities.
|
|
|
|
**Key Features Demonstrated:**
|
|
- ✅ Paginated data fetching with search and filters
|
|
- ✅ Batch selection
|
|
- ✅ Toast notifications
|
|
- ✅ Data table with sorting
|
|
- ✅ Export to CSV
|
|
- ✅ Delete operations
|
|
- ✅ Loading and error states
|
|
|
|
---
|
|
|
|
## Benefits
|
|
|
|
### Before (Without Utilities)
|
|
|
|
```tsx
|
|
// 100+ lines of boilerplate for a list page
|
|
function ArticleList() {
|
|
const [articles, setArticles] = useState([]);
|
|
const [page, setPage] = useState(1);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const [selected, setSelected] = useState([]);
|
|
const [search, setSearch] = useState('');
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
fetch(`/api/articles?page=${page}&search=${search}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
setArticles(data.data);
|
|
setLoading(false);
|
|
})
|
|
.catch(err => {
|
|
setError(err.message);
|
|
setLoading(false);
|
|
});
|
|
}, [page, search]);
|
|
|
|
// More boilerplate...
|
|
}
|
|
```
|
|
|
|
### After (With Utilities)
|
|
|
|
```tsx
|
|
// 30 lines with full functionality
|
|
function ArticleList() {
|
|
const { data, meta, loading, error, setSearch, setPage } =
|
|
usePaginatedData('/articles');
|
|
const selection = useBatchSelection(data, 'id');
|
|
const toast = useToast();
|
|
|
|
return (
|
|
<div>
|
|
<ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />
|
|
<DataTable
|
|
data={data}
|
|
columns={columns}
|
|
loading={loading}
|
|
selectable
|
|
{...selection}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Result:** 70% less code, more features, better UX!
|
|
|
|
---
|
|
|
|
## Files Created
|
|
|
|
**Hooks:**
|
|
1. ✅ `hooks/usePaginatedData.ts` - Paginated data fetching
|
|
2. ✅ `hooks/useApiMutation.ts` - API mutations (POST, PUT, DELETE)
|
|
3. ✅ `hooks/useFormValidation.ts` - Form validation
|
|
4. ✅ `hooks/useQueryBuilder.ts` - Query string builder
|
|
5. ✅ `hooks/useToast.ts` - Toast notifications
|
|
6. ✅ `hooks/useBatchSelection.ts` - Batch selection
|
|
|
|
**Components:**
|
|
7. ✅ `components/common/DataTable.tsx` - Data table component
|
|
8. ✅ `components/common/DataTable.css` - Table styles
|
|
9. ✅ `components/common/ToastContainer.tsx` - Toast container
|
|
10. ✅ `components/common/ToastContainer.css` - Toast styles
|
|
|
|
**Utilities:**
|
|
11. ✅ `utils/export.ts` - Export functions
|
|
|
|
**Examples:**
|
|
12. ✅ `components/examples/ArticleListExample.tsx` - Complete example
|
|
13. ✅ `components/examples/ArticleListExample.css` - Example styles
|
|
|
|
**Documentation:**
|
|
14. ✅ `FRONTEND_UTILITIES_GUIDE.md` - This guide
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
```tsx
|
|
// Fetch paginated data
|
|
const { data, meta, loading, setSearch, setPage } =
|
|
usePaginatedData<T>('/endpoint');
|
|
|
|
// API mutations
|
|
const { mutate, loading, error } = useApiPost<T>('/endpoint');
|
|
|
|
// Form validation
|
|
const { values, errors, handleChange, handleSubmit } =
|
|
useFormValidation(initialValues, rules);
|
|
|
|
// Query builder
|
|
const { queryString, setFilter, setSearch, setSort } = useQueryBuilder();
|
|
|
|
// Toast notifications
|
|
const toast = useToast();
|
|
toast.success('Success!');
|
|
toast.error('Error!');
|
|
|
|
// Batch selection
|
|
const selection = useBatchSelection(items, 'id');
|
|
selection.toggle(id);
|
|
selection.selectAll();
|
|
|
|
// Export
|
|
exportToCSV(data, 'export.csv');
|
|
exportToJSON(data, 'export.json');
|
|
```
|
|
|
|
---
|
|
|
|
## Integration with Backend
|
|
|
|
All frontend utilities are designed to work seamlessly with the backend helpers:
|
|
|
|
| Frontend | Backend | Purpose |
|
|
|----------|---------|---------|
|
|
| `usePaginatedData` | `Paginator` | Pagination |
|
|
| `useQueryBuilder` | `QueryParser` | Filtering/Sorting |
|
|
| `useFormValidation` | `Validator` | Validation |
|
|
| `useToast` | `Respond` | User Feedback |
|
|
| `useBatchSelection` | `BatchOps` | Batch Operations |
|
|
| `exportToCSV` | `Exporter` | Data Export |
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Always use error boundaries** with data fetching hooks
|
|
2. **Debounce search inputs** to avoid excessive API calls
|
|
3. **Show loading states** for better UX
|
|
4. **Use toast notifications** for user feedback
|
|
5. **Implement batch operations** for efficiency
|
|
6. **Export functionality** for reporting needs
|
|
7. **TypeScript types** for type safety
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. Review the complete example: `ArticleListExample.tsx`
|
|
2. Copy patterns for your own components
|
|
3. Customize styles to match your design
|
|
4. Add more custom validators as needed
|
|
5. Extend utilities with project-specific features
|
|
|
|
**Your frontend development is now easier, faster, and better!** 🚀
|