mirror of
https://github.com/johndoe6345789/tsmorph.git
synced 2026-04-24 13:54:58 +00:00
Add initial TSX component for refactoring demonstration
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
26
package.json
Normal file
26
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
602
src/components/UserManagementDashboard.tsx
Normal file
602
src/components/UserManagementDashboard.tsx
Normal file
@@ -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<User[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterRole, setFilterRole] = useState<string>('all');
|
||||
const [sortField, setSortField] = useState<keyof User>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
role: 'user',
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState<ValidationErrors>({});
|
||||
|
||||
// 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<HTMLInputElement | HTMLSelectElement>) => {
|
||||
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 (
|
||||
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1>User Management Dashboard</h1>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
backgroundColor: '#fee',
|
||||
color: '#c33',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search and Filter Controls */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
marginBottom: '20px',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search users..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
flex: '1',
|
||||
minWidth: '200px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={filterRole}
|
||||
onChange={(e) => setFilterRole(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
<option value="guest">Guest</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowForm(true);
|
||||
setEditingUser(null);
|
||||
setFormData({ name: '', email: '', role: 'user' });
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#3498db',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* User Form */}
|
||||
{showForm && (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<h2>{editingUser ? 'Edit User' : 'Add New User'}</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
Name:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: `1px solid ${formErrors.name ? '#e74c3c' : '#ddd'}`,
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
/>
|
||||
{formErrors.name && (
|
||||
<span style={{ color: '#e74c3c', fontSize: '14px' }}>
|
||||
{formErrors.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
Email:
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: `1px solid ${formErrors.email ? '#e74c3c' : '#ddd'}`,
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
/>
|
||||
{formErrors.email && (
|
||||
<span style={{ color: '#e74c3c', fontSize: '14px' }}>
|
||||
{formErrors.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
Role:
|
||||
</label>
|
||||
<select
|
||||
name="role"
|
||||
value={formData.role}
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="guest">Guest</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#2ecc71',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowForm(false);
|
||||
setEditingUser(null);
|
||||
setFormData({ name: '', email: '', role: 'user' });
|
||||
setFormErrors({});
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#95a5a6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Users Table */}
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
{loading && users.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
Loading users...
|
||||
</div>
|
||||
) : (
|
||||
<table style={{
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#f8f9fa' }}>
|
||||
<th
|
||||
onClick={() => handleSort('name')}
|
||||
style={{
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid #dee2e6'
|
||||
}}
|
||||
>
|
||||
Name {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleSort('email')}
|
||||
style={{
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid #dee2e6'
|
||||
}}
|
||||
>
|
||||
Email {sortField === 'email' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleSort('role')}
|
||||
style={{
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid #dee2e6'
|
||||
}}
|
||||
>
|
||||
Role {sortField === 'role' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleSort('status')}
|
||||
style={{
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid #dee2e6'
|
||||
}}
|
||||
>
|
||||
Status {sortField === 'status' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
onClick={() => handleSort('createdAt')}
|
||||
style={{
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '2px solid #dee2e6'
|
||||
}}
|
||||
>
|
||||
Created {sortField === 'createdAt' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th style={{
|
||||
padding: '12px',
|
||||
textAlign: 'left',
|
||||
borderBottom: '2px solid #dee2e6'
|
||||
}}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredAndSortedUsers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} style={{
|
||||
padding: '40px',
|
||||
textAlign: 'center',
|
||||
color: '#7f8c8d'
|
||||
}}>
|
||||
No users found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredAndSortedUsers.map(user => (
|
||||
<tr key={user.id} style={{ borderBottom: '1px solid #dee2e6' }}>
|
||||
<td style={{ padding: '12px' }}>{user.name}</td>
|
||||
<td style={{ padding: '12px' }}>{user.email}</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<span style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: getRoleBadgeColor(user.role),
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{user.role.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<span style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: getStatusBadgeColor(user.status),
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{user.status.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px' }}>{formatDate(user.createdAt)}</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<button
|
||||
onClick={() => handleEdit(user)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
marginRight: '5px',
|
||||
backgroundColor: '#3498db',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(user.id)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
backgroundColor: '#e74c3c',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '20px', color: '#7f8c8d', fontSize: '14px' }}>
|
||||
Showing {filteredAndSortedUsers.length} of {users.length} users
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagementDashboard;
|
||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user