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

14 KiB

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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.