diff --git a/package.json b/package.json new file mode 100644 index 0000000..f55b0d4 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "tsmorph", + "version": "1.0.0", + "description": "TSX refactoring demonstration - Extract code blocks >150 LOC", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint . --ext .ts,.tsx", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "next": "^14.0.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "eslint": "^8.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0" + } +} diff --git a/src/components/UserManagementDashboard.tsx b/src/components/UserManagementDashboard.tsx new file mode 100644 index 0000000..20d73a4 --- /dev/null +++ b/src/components/UserManagementDashboard.tsx @@ -0,0 +1,602 @@ +import React, { useState, useEffect, useCallback } from 'react'; + +/** + * BEFORE REFACTORING: Large Monolithic Component (~200+ LOC) + * This component handles user management with forms, tables, and API calls + * all in one file - a common anti-pattern that needs refactoring + */ + +interface User { + id: string; + name: string; + email: string; + role: 'admin' | 'user' | 'guest'; + status: 'active' | 'inactive'; + createdAt: string; +} + +interface FormData { + name: string; + email: string; + role: 'admin' | 'user' | 'guest'; +} + +interface ValidationErrors { + name?: string; + email?: string; + role?: string; +} + +export const UserManagementDashboard: React.FC = () => { + // State management + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showForm, setShowForm] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [filterRole, setFilterRole] = useState('all'); + const [sortField, setSortField] = useState('name'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + + // Form state + const [formData, setFormData] = useState({ + name: '', + email: '', + role: 'user', + }); + const [formErrors, setFormErrors] = useState({}); + + // Validation logic + const validateForm = (data: FormData): ValidationErrors => { + const errors: ValidationErrors = {}; + + if (!data.name.trim()) { + errors.name = 'Name is required'; + } else if (data.name.length < 2) { + errors.name = 'Name must be at least 2 characters'; + } else if (data.name.length > 50) { + errors.name = 'Name must be less than 50 characters'; + } + + if (!data.email.trim()) { + errors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { + errors.email = 'Invalid email format'; + } + + if (!data.role) { + errors.role = 'Role is required'; + } + + return errors; + }; + + // API simulation functions + const fetchUsers = useCallback(async () => { + setLoading(true); + setError(null); + try { + // Simulated API call + await new Promise(resolve => setTimeout(resolve, 500)); + const mockUsers: User[] = [ + { + id: '1', + name: 'John Doe', + email: 'john@example.com', + role: 'admin', + status: 'active', + createdAt: '2024-01-15T10:00:00Z', + }, + { + id: '2', + name: 'Jane Smith', + email: 'jane@example.com', + role: 'user', + status: 'active', + createdAt: '2024-02-20T14:30:00Z', + }, + { + id: '3', + name: 'Bob Johnson', + email: 'bob@example.com', + role: 'guest', + status: 'inactive', + createdAt: '2024-03-10T09:15:00Z', + }, + ]; + setUsers(mockUsers); + } catch (err) { + setError('Failed to fetch users'); + console.error(err); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchUsers(); + }, [fetchUsers]); + + // Form handlers + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + // Clear error for this field + if (formErrors[name as keyof ValidationErrors]) { + setFormErrors(prev => ({ ...prev, [name]: undefined })); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const errors = validateForm(formData); + if (Object.keys(errors).length > 0) { + setFormErrors(errors); + return; + } + + setLoading(true); + try { + // Simulated API call + await new Promise(resolve => setTimeout(resolve, 500)); + + if (editingUser) { + // Update existing user + setUsers(prev => prev.map(user => + user.id === editingUser.id + ? { ...user, ...formData } + : user + )); + } else { + // Add new user + const newUser: User = { + id: Date.now().toString(), + ...formData, + status: 'active', + createdAt: new Date().toISOString(), + }; + setUsers(prev => [...prev, newUser]); + } + + // Reset form + setFormData({ name: '', email: '', role: 'user' }); + setShowForm(false); + setEditingUser(null); + } catch (err) { + setError('Failed to save user'); + console.error(err); + } finally { + setLoading(false); + } + }; + + const handleEdit = (user: User) => { + setEditingUser(user); + setFormData({ + name: user.name, + email: user.email, + role: user.role, + }); + setShowForm(true); + }; + + const handleDelete = async (userId: string) => { + if (!window.confirm('Are you sure you want to delete this user?')) { + return; + } + + setLoading(true); + try { + // Simulated API call + await new Promise(resolve => setTimeout(resolve, 300)); + setUsers(prev => prev.filter(user => user.id !== userId)); + } catch (err) { + setError('Failed to delete user'); + console.error(err); + } finally { + setLoading(false); + } + }; + + // Filtering and sorting logic + const filteredAndSortedUsers = users + .filter(user => { + const matchesSearch = user.name.toLowerCase().includes(searchTerm.toLowerCase()) || + user.email.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesRole = filterRole === 'all' || user.role === filterRole; + return matchesSearch && matchesRole; + }) + .sort((a, b) => { + const aValue = a[sortField]; + const bValue = b[sortField]; + const comparison = aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + return sortDirection === 'asc' ? comparison : -comparison; + }); + + const handleSort = (field: keyof User) => { + if (sortField === field) { + setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortDirection('asc'); + } + }; + + // Render helpers + const getRoleBadgeColor = (role: string) => { + switch (role) { + case 'admin': return '#ff6b6b'; + case 'user': return '#4ecdc4'; + case 'guest': return '#95a5a6'; + default: return '#7f8c8d'; + } + }; + + const getStatusBadgeColor = (status: string) => { + return status === 'active' ? '#2ecc71' : '#e74c3c'; + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + return ( +
+

User Management Dashboard

+ + {error && ( +
+ {error} +
+ )} + + {/* Search and Filter Controls */} +
+ setSearchTerm(e.target.value)} + style={{ + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + flex: '1', + minWidth: '200px' + }} + /> + + + + +
+ + {/* User Form */} + {showForm && ( +
+

{editingUser ? 'Edit User' : 'Add New User'}

+
+
+ + + {formErrors.name && ( + + {formErrors.name} + + )} +
+ +
+ + + {formErrors.email && ( + + {formErrors.email} + + )} +
+ +
+ + +
+ +
+ + +
+
+
+ )} + + {/* Users Table */} +
+ {loading && users.length === 0 ? ( +
+ Loading users... +
+ ) : ( + + + + + + + + + + + + + {filteredAndSortedUsers.length === 0 ? ( + + + + ) : ( + filteredAndSortedUsers.map(user => ( + + + + + + + + + )) + )} + +
handleSort('name')} + style={{ + padding: '12px', + textAlign: 'left', + cursor: 'pointer', + borderBottom: '2px solid #dee2e6' + }} + > + Name {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')} + handleSort('email')} + style={{ + padding: '12px', + textAlign: 'left', + cursor: 'pointer', + borderBottom: '2px solid #dee2e6' + }} + > + Email {sortField === 'email' && (sortDirection === 'asc' ? '↑' : '↓')} + handleSort('role')} + style={{ + padding: '12px', + textAlign: 'left', + cursor: 'pointer', + borderBottom: '2px solid #dee2e6' + }} + > + Role {sortField === 'role' && (sortDirection === 'asc' ? '↑' : '↓')} + handleSort('status')} + style={{ + padding: '12px', + textAlign: 'left', + cursor: 'pointer', + borderBottom: '2px solid #dee2e6' + }} + > + Status {sortField === 'status' && (sortDirection === 'asc' ? '↑' : '↓')} + handleSort('createdAt')} + style={{ + padding: '12px', + textAlign: 'left', + cursor: 'pointer', + borderBottom: '2px solid #dee2e6' + }} + > + Created {sortField === 'createdAt' && (sortDirection === 'asc' ? '↑' : '↓')} + + Actions +
+ No users found +
{user.name}{user.email} + + {user.role.toUpperCase()} + + + + {user.status.toUpperCase()} + + {formatDate(user.createdAt)} + + +
+ )} +
+ +
+ Showing {filteredAndSortedUsers.length} of {users.length} users +
+
+ ); +}; + +export default UserManagementDashboard; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..152bdd9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}