mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
dev day #65
This commit is contained in:
@@ -0,0 +1,711 @@
|
||||
# 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!** 🚀
|
||||
Reference in New Issue
Block a user