Implement professional error handling and user feedback system
Some checks failed
Lint and Build / build (20) (pull_request) Failing after 35s
Lint and Build / build (22) (pull_request) Failing after 51s

Major UX Improvements:
- Replace all alert() dialogs with elegant toast notifications
- Add comprehensive loading states for all async operations
- Implement client-side form validation with visual feedback
- Add React Error Boundary for graceful error recovery

Components Added:
- Toast notification system with success/error/warning/info types
- useToast hook for easy notification management
- ErrorBoundary component with development error details
- Form validation with real-time feedback

User Experience:
- Professional toast notifications instead of browser alerts
- Loading indicators prevent double-clicks and show progress
- Immediate validation feedback prevents submission errors
- Graceful error recovery with retry options
- Enhanced accessibility with proper ARIA labels

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Decobus 2025-07-19 05:39:21 -04:00
parent dc1e3a62a1
commit b6ff9a3cb6
5 changed files with 350 additions and 26 deletions

View file

@ -2,6 +2,8 @@
import { useState, useEffect } from 'react';
import { Team } from '@/types';
import { useToast } from '@/lib/useToast';
import { ToastContainer } from '@/components/Toast';
export default function Teams() {
const [teams, setTeams] = useState<Team[]>([]);
@ -10,6 +12,10 @@ export default function Teams() {
const [editingTeam, setEditingTeam] = useState<Team | null>(null);
const [editingName, setEditingName] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [updatingTeamId, setUpdatingTeamId] = useState<number | null>(null);
const [deletingTeamId, setDeletingTeamId] = useState<number | null>(null);
const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
const { toasts, removeToast, showSuccess, showError } = useToast();
useEffect(() => {
fetchTeams();
@ -30,7 +36,22 @@ export default function Teams() {
const handleAddTeam = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTeamName.trim()) return;
// Client-side validation
const errors: {[key: string]: string} = {};
if (!newTeamName.trim()) {
errors.newTeamName = 'Team name is required';
} else if (newTeamName.trim().length < 2) {
errors.newTeamName = 'Team name must be at least 2 characters';
} else if (newTeamName.trim().length > 50) {
errors.newTeamName = 'Team name must be less than 50 characters';
}
setValidationErrors(errors);
if (Object.keys(errors).length > 0) {
showError('Validation Error', 'Please fix the form errors');
return;
}
setIsSubmitting(true);
try {
@ -43,21 +64,35 @@ export default function Teams() {
if (res.ok) {
setNewTeamName('');
fetchTeams();
showSuccess('Team Added', `"${newTeamName}" has been added successfully`);
} else {
const error = await res.json();
alert(`Error adding team: ${error.error}`);
showError('Failed to Add Team', error.error || 'Unknown error occurred');
}
} catch (error) {
console.error('Error adding team:', error);
alert('Failed to add team');
showError('Failed to Add Team', 'Network error or server unavailable');
} finally {
setIsSubmitting(false);
}
};
const handleUpdateTeam = async (teamId: number) => {
if (!editingName.trim()) return;
// Client-side validation
if (!editingName.trim()) {
showError('Validation Error', 'Team name cannot be empty');
return;
}
if (editingName.trim().length < 2) {
showError('Validation Error', 'Team name must be at least 2 characters');
return;
}
if (editingName.trim().length > 50) {
showError('Validation Error', 'Team name must be less than 50 characters');
return;
}
setUpdatingTeamId(teamId);
try {
const res = await fetch(`/api/teams/${teamId}`, {
method: 'PUT',
@ -69,21 +104,26 @@ export default function Teams() {
setEditingTeam(null);
setEditingName('');
fetchTeams();
showSuccess('Team Updated', `Team name changed to "${editingName}"`);
} else {
const error = await res.json();
alert(`Error updating team: ${error.error}`);
showError('Failed to Update Team', error.error || 'Unknown error occurred');
}
} catch (error) {
console.error('Error updating team:', error);
alert('Failed to update team');
showError('Failed to Update Team', 'Network error or server unavailable');
} finally {
setUpdatingTeamId(null);
}
};
const handleDeleteTeam = async (teamId: number) => {
const teamToDelete = teams.find(t => t.team_id === teamId);
if (!confirm('Are you sure you want to delete this team? This will also delete all associated streams.')) {
return;
}
setDeletingTeamId(teamId);
try {
const res = await fetch(`/api/teams/${teamId}`, {
method: 'DELETE',
@ -91,13 +131,16 @@ export default function Teams() {
if (res.ok) {
fetchTeams();
showSuccess('Team Deleted', `"${teamToDelete?.team_name || 'Team'}" has been deleted`);
} else {
const error = await res.json();
alert(`Error deleting team: ${error.error}`);
showError('Failed to Delete Team', error.error || 'Unknown error occurred');
}
} catch (error) {
console.error('Error deleting team:', error);
alert('Failed to delete team');
showError('Failed to Delete Team', 'Network error or server unavailable');
} finally {
setDeletingTeamId(null);
}
};
@ -125,24 +168,39 @@ export default function Teams() {
<div className="glass p-6 mb-6">
<h2 className="card-title">Add New Team</h2>
<form onSubmit={handleAddTeam} className="max-w-md mx-auto">
<div className="form-row">
<input
type="text"
value={newTeamName}
onChange={(e) => setNewTeamName(e.target.value)}
placeholder="Enter team name"
className="input"
style={{ flex: 1 }}
required
/>
<button
type="submit"
disabled={isSubmitting}
className="btn btn-success"
<div>
<div className="form-row">
<input
type="text"
value={newTeamName}
onChange={(e) => {
setNewTeamName(e.target.value);
// Clear validation error when user starts typing
if (validationErrors.newTeamName) {
setValidationErrors(prev => ({ ...prev, newTeamName: '' }));
}
}}
placeholder="Enter team name"
className={`input ${
validationErrors.newTeamName ? 'border-red-500/60 bg-red-500/10' : ''
}`}
style={{ flex: 1 }}
required
/>
<button
type="submit"
disabled={isSubmitting}
className="btn btn-success"
>
<span className="icon"></span>
{isSubmitting ? 'Adding...' : 'Add Team'}
</button>
</button>
</div>
{validationErrors.newTeamName && (
<div className="text-red-400 text-sm mt-2 text-center">
{validationErrors.newTeamName}
</div>
)}
</div>
</form>
</div>
@ -180,14 +238,16 @@ export default function Teams() {
/>
<button
onClick={() => handleUpdateTeam(team.team_id)}
disabled={updatingTeamId === team.team_id}
className="btn btn-success btn-sm"
title="Save changes"
>
<span className="icon"></span>
Save
{updatingTeamId === team.team_id ? 'Saving...' : 'Save'}
</button>
<button
onClick={cancelEditing}
disabled={updatingTeamId === team.team_id}
className="btn-secondary btn-sm"
title="Cancel editing"
>
@ -209,6 +269,7 @@ export default function Teams() {
<div className="button-group">
<button
onClick={() => startEditing(team)}
disabled={deletingTeamId === team.team_id || updatingTeamId === team.team_id}
className="btn-secondary btn-sm"
title="Edit team"
>
@ -217,11 +278,12 @@ export default function Teams() {
</button>
<button
onClick={() => handleDeleteTeam(team.team_id)}
disabled={deletingTeamId === team.team_id || updatingTeamId === team.team_id}
className="btn-danger btn-sm"
title="Delete team"
>
<span className="icon">🗑</span>
Delete
{deletingTeamId === team.team_id ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
@ -231,6 +293,9 @@ export default function Teams() {
</div>
)}
</div>
{/* Toast Notifications */}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</div>
);
}