Assist_Design/EXAMPLE_USAGE.md
T. Narantuya a95ec60859 Refactor address management and update related services for improved clarity and functionality
- Updated address retrieval in user service to replace billing info with a dedicated address method.
- Adjusted API endpoints to use `PATCH /api/me/address` for address updates instead of billing updates.
- Enhanced documentation to reflect changes in address management processes and API usage.
- Removed deprecated types and services related to billing address handling, streamlining the codebase.
2025-09-17 18:43:43 +09:00

566 lines
14 KiB
Markdown

# Example Usage of New Type Patterns
This document shows practical examples of how to use the new unified type patterns from `@customer-portal/domain`.
## 1. Basic Async State Usage
### Custom Hook with New Async State
```typescript
// hooks/useUser.ts
import { useState, useEffect } from 'react';
import {
AsyncState,
createIdleState,
createLoadingState,
createSuccessState,
createErrorState,
type User
} from '@customer-portal/domain';
export function useUser(userId: string) {
const [state, setState] = useState<AsyncState<User>>(createIdleState());
useEffect(() => {
if (!userId) return;
setState(createLoadingState());
fetchUser(userId)
.then(user => setState(createSuccessState(user)))
.catch(error => setState(createErrorState(error.message)));
}, [userId]);
return state;
}
```
### Component Using Async State
```typescript
// components/UserProfile.tsx
import React from 'react';
import { isLoading, isError, isSuccess, isIdle } from '@customer-portal/domain';
import { useUser } from '../hooks/useUser';
interface UserProfileProps {
userId: string;
}
export function UserProfile({ userId }: UserProfileProps) {
const userState = useUser(userId);
if (isIdle(userState)) {
return <div>Ready to load user...</div>;
}
if (isLoading(userState)) {
return <div className="spinner">Loading user...</div>;
}
if (isError(userState)) {
return (
<div className="error">
<h3>Error loading user</h3>
<p>{userState.error}</p>
</div>
);
}
if (isSuccess(userState)) {
return (
<div className="user-profile">
<h2>{userState.data.name}</h2>
<p>Email: {userState.data.email}</p>
<p>Phone: {userState.data.phone}</p>
</div>
);
}
// This should never happen with discriminated unions
return null;
}
```
## 2. Form State Usage
### Custom Form Hook
```typescript
// hooks/useLoginForm.ts
import { useState } from 'react';
import {
FormState,
createFormState,
updateFormField,
getFormValues,
hasFormErrors,
isFormDirty
} from '@customer-portal/domain';
interface LoginFormData {
email: string;
password: string;
}
export function useLoginForm() {
const [formState, setFormState] = useState<FormState<LoginFormData>>(
createFormState({
email: '',
password: '',
})
);
const updateField = (field: keyof LoginFormData, value: string, error?: string) => {
setFormState(prev => ({
...prev,
[field]: updateFormField(prev[field], value, error),
isValid: !error && !hasFormErrors(prev),
}));
};
const validateEmail = (email: string): string | undefined => {
if (!email) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(email)) return 'Email is invalid';
return undefined;
};
const validatePassword = (password: string): string | undefined => {
if (!password) return 'Password is required';
if (password.length < 8) return 'Password must be at least 8 characters';
return undefined;
};
const handleEmailChange = (email: string) => {
const error = validateEmail(email);
updateField('email', email, error);
};
const handlePasswordChange = (password: string) => {
const error = validatePassword(password);
updateField('password', password, error);
};
const handleSubmit = async () => {
const formData = getFormValues(formState);
// Validate all fields
const emailError = validateEmail(formData.email);
const passwordError = validatePassword(formData.password);
if (emailError || passwordError) {
setFormState(prev => ({
...prev,
email: { ...prev.email, error: emailError },
password: { ...prev.password, error: passwordError },
isValid: false,
}));
return;
}
setFormState(prev => ({ ...prev, isSubmitting: true }));
try {
await login(formData);
// Handle success
} catch (error) {
// Handle error
setFormState(prev => ({
...prev,
isSubmitting: false,
errors: { email: 'Login failed. Please check your credentials.' }
}));
}
};
return {
formState,
handleEmailChange,
handlePasswordChange,
handleSubmit,
isValid: formState.isValid && !hasFormErrors(formState),
isDirty: isFormDirty(formState),
};
}
```
### Login Form Component
```typescript
// components/LoginForm.tsx
import React from 'react';
import { useLoginForm } from '../hooks/useLoginForm';
export function LoginForm() {
const {
formState,
handleEmailChange,
handlePasswordChange,
handleSubmit,
isValid,
isDirty,
} = useLoginForm();
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div className="form-field">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={formState.email.value}
onChange={(e) => handleEmailChange(e.target.value)}
className={formState.email.error ? 'error' : ''}
/>
{formState.email.error && (
<span className="error-message">{formState.email.error}</span>
)}
</div>
<div className="form-field">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={formState.password.value}
onChange={(e) => handlePasswordChange(e.target.value)}
className={formState.password.error ? 'error' : ''}
/>
{formState.password.error && (
<span className="error-message">{formState.password.error}</span>
)}
</div>
<button
type="submit"
disabled={!isValid || formState.isSubmitting}
className="submit-button"
>
{formState.isSubmitting ? 'Logging in...' : 'Login'}
</button>
{isDirty && (
<p className="form-hint">You have unsaved changes</p>
)}
</form>
);
}
```
## 3. API Integration with Adapters
### Service Layer with Adapters
```typescript
// services/userService.ts
import {
adaptWhmcsResponse,
adaptSalesforceResponse,
unwrapResponse,
isSuccessResponse,
type ApiResponse,
type User,
type WhmcsClientId,
createWhmcsClientId
} from '@customer-portal/domain';
class UserService {
async getUser(userId: string): Promise<User> {
// Get user from WHMCS
const whmcsResponse = await this.whmcsApi.getClient(
createWhmcsClientId(parseInt(userId))
);
const unifiedResponse = adaptWhmcsResponse(whmcsResponse);
return unwrapResponse(unifiedResponse);
}
async updateUser(userId: string, userData: Partial<User>): Promise<User> {
// Update in both WHMCS and Salesforce
const [whmcsResponse, sfResponse] = await Promise.all([
this.whmcsApi.updateClient(createWhmcsClientId(parseInt(userId)), userData),
this.salesforceApi.updateContact(userId, userData),
]);
const whmcsResult = adaptWhmcsResponse(whmcsResponse);
const sfResult = adaptSalesforceResponse(sfResponse);
// Check both responses
if (!isSuccessResponse(whmcsResult)) {
throw new Error(`WHMCS update failed: ${whmcsResult.error.message}`);
}
if (!isSuccessResponse(sfResult)) {
throw new Error(`Salesforce update failed: ${sfResult.error.message}`);
}
return whmcsResult.data;
}
async searchUsers(query: string): Promise<User[]> {
try {
const response = await this.api.searchUsers(query);
const adaptedResponse = adaptWhmcsResponse(response);
return unwrapResponse(adaptedResponse);
} catch (error) {
console.error('User search failed:', error);
return [];
}
}
}
export const userService = new UserService();
```
### Hook with API Integration
```typescript
// hooks/useUserSearch.ts
import { useState, useCallback } from 'react';
import {
AsyncState,
createIdleState,
createLoadingState,
createSuccessState,
createErrorState,
type User
} from '@customer-portal/domain';
import { userService } from '../services/userService';
export function useUserSearch() {
const [searchState, setSearchState] = useState<AsyncState<User[]>>(createIdleState());
const [query, setQuery] = useState('');
const searchUsers = useCallback(async (searchQuery: string) => {
if (!searchQuery.trim()) {
setSearchState(createIdleState());
return;
}
setSearchState(createLoadingState());
try {
const users = await userService.searchUsers(searchQuery);
setSearchState(createSuccessState(users));
} catch (error) {
setSearchState(createErrorState(error instanceof Error ? error.message : 'Search failed'));
}
}, []);
return {
searchState,
query,
setQuery,
searchUsers,
};
}
```
## 4. Paginated Data Usage
### Paginated List Hook
```typescript
// hooks/useUserList.ts
import { useState, useEffect, useCallback } from 'react';
import {
PaginatedAsyncState,
PaginationParams,
createIdleState,
createLoadingState,
createSuccessState,
createErrorState,
validatePaginationParams,
type User
} from '@customer-portal/domain';
export function useUserList(initialParams: PaginationParams = {}) {
const [state, setState] = useState<PaginatedAsyncState<User>>(createIdleState());
const [params, setParams] = useState(() => validatePaginationParams(initialParams));
const loadUsers = useCallback(async (paginationParams: PaginationParams) => {
setState(createLoadingState());
try {
const validatedParams = validatePaginationParams(paginationParams);
const response = await userService.getUsers(validatedParams);
setState({
status: 'success',
data: response.data,
pagination: response.pagination,
});
} catch (error) {
setState(createErrorState(error instanceof Error ? error.message : 'Failed to load users'));
}
}, []);
const goToPage = useCallback((page: number) => {
const newParams = { ...params, page };
setParams(newParams);
loadUsers(newParams);
}, [params, loadUsers]);
const changePageSize = useCallback((limit: number) => {
const newParams = { ...params, limit, page: 1 };
setParams(newParams);
loadUsers(newParams);
}, [params, loadUsers]);
useEffect(() => {
loadUsers(params);
}, [loadUsers, params]);
return {
state,
params,
goToPage,
changePageSize,
reload: () => loadUsers(params),
};
}
```
### Paginated List Component
```typescript
// components/UserList.tsx
import React from 'react';
import { isLoading, isError, isSuccess } from '@customer-portal/domain';
import { useUserList } from '../hooks/useUserList';
export function UserList() {
const { state, params, goToPage, changePageSize, reload } = useUserList({
page: 1,
limit: 10,
});
if (isLoading(state)) {
return <div className="loading">Loading users...</div>;
}
if (isError(state)) {
return (
<div className="error">
<p>Error: {state.error}</p>
<button onClick={reload}>Retry</button>
</div>
);
}
if (isSuccess(state)) {
return (
<div className="user-list">
<div className="user-list-header">
<h2>Users ({state.pagination.total})</h2>
<select
value={params.limit}
onChange={(e) => changePageSize(Number(e.target.value))}
>
<option value={10}>10 per page</option>
<option value={25}>25 per page</option>
<option value={50}>50 per page</option>
</select>
</div>
<div className="users">
{state.data.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
<div className="pagination">
<button
disabled={!state.pagination.hasPrev}
onClick={() => goToPage(params.page - 1)}
>
Previous
</button>
<span>
Page {state.pagination.page} of {state.pagination.totalPages}
</span>
<button
disabled={!state.pagination.hasNext}
onClick={() => goToPage(params.page + 1)}
>
Next
</button>
</div>
</div>
);
}
return null;
}
```
## 5. Selection State Usage
### Selection Hook
```typescript
// hooks/useSelection.ts
import { useState, useCallback } from 'react';
import {
SelectionState,
createSelectionState,
updateSelectionState
} from '@customer-portal/domain';
export function useSelection<T>() {
const [selectionState, setSelectionState] = useState<SelectionState<T>>(
createSelectionState()
);
const toggleItem = useCallback((item: T) => {
setSelectionState(prev => {
const isSelected = prev.selected.includes(item);
return updateSelectionState(prev, item, !isSelected);
});
}, []);
const selectAll = useCallback((items: T[]) => {
setSelectionState({
selected: [...items],
selectAll: true,
indeterminate: false,
});
}, []);
const deselectAll = useCallback(() => {
setSelectionState(createSelectionState());
}, []);
return {
selectionState,
toggleItem,
selectAll,
deselectAll,
hasSelection: selectionState.selected.length > 0,
selectedCount: selectionState.selected.length,
};
}
```
## Key Benefits Demonstrated
1. **Type Safety**: Impossible states are eliminated
2. **Consistency**: Same patterns across all components
3. **Reusability**: Hooks and utilities can be shared
4. **Maintainability**: Clear separation of concerns
5. **Developer Experience**: Better IntelliSense and debugging
## Next Steps
1. Use these patterns in your new components
2. Gradually migrate existing components
3. Add tests for your hooks and components
4. Customize the patterns for your specific needs
These examples show how the new type patterns lead to more robust, maintainable, and type-safe code while providing an excellent developer experience.