To set up and build a React + TypeScript project named emsv2025-ts
using React 18.3.1. This includes:
✅ Project setup with essential dependencies
📁 Recommended folder structure
🧠 Explanation of
App.tsx
, routing, andNaviBar.tsx
,EmployeeList.tsx
components
✅ 1. Project Setup: EMSV2025 (React + TypeScript)
📁 2. Folder Structure
🧠 3. App.tsx
(Main Router Configuration)
// Import React core library
import React from "react";
// Import routing components from react-router-dom
import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom";
// Import Font Awesome CSS (commented out Bootstrap import shows correct import order)
import "@fortawesome/fontawesome-free/css/all.min.css";
// Import application components
import AddEmployee from "./components/AddEnployee";
import EmployeeList from "./components/EmployeeList";
import EditEmployee from "./components/EditEmployee";
import NaviBar from "./components/NaviBar";
// Main content component that handles route-specific logic
const AppContent: React.FC = () => {
// Temporary hardcoded user (would normally come from auth context)
const user = "Sanjay";
// Get current location using react-router's useLocation hook
const location = useLocation();
// Determine if header should be shown (not on login page)
const showHeader = location.pathname !== "/login";
return (
// React Fragment (<> </>) to group elements without extra DOM node
<>
{/* Conditionally render NaviBar based on route */}
{showHeader && <NaviBar />}
{/* Main content container with top margin */}
<div className="container mt-4">
{/* Routes component defines all route paths */}
<Routes>
{/* Default route (/) shows EmployeeList */}
<Route path="/" element={<EmployeeList />} />
{/* Alternate path also showing EmployeeList */}
<Route path="/list" element={<EmployeeList />} />
{/* Add employee form route */}
<Route path="/add" element={<AddEmployee />} />
{/* Edit employee route with dynamic parameter (empId) */}
<Route path="/edit/:empId" element={<EditEmployee />} />
</Routes>
</div>
</>
);
};
// Root application component that wraps everything with Router
const App: React.FC = () => (
{/* Router component provides routing context to all child components */}
<Router>
<AppContent />
</Router>
);
// Export the App component as default
export default App;
Key Features Explained:
Routing Setup:
Router
wraps the entire application to enable client-side routingRoutes
defines all possible route pathsRoute
components map paths to specific components
Conditional Rendering:
Uses
useLocation
to check current pathConditionally shows/hides
NaviBar
based on route (would hide on/login
)
Route Parameters:
Dynamic route
/edit/:empId
captures employee ID in URLThe
EditEmployee
component can access this viauseParams()
Layout Structure:
Navbar appears at top (when shown)
Main content has consistent container styling with
mt-4
margin
Scalability:
Easy to add new routes by importing components and adding Route entries
Clear separation between layout (AppContent) and routing setup (App)
Important Notes:
The commented out Bootstrap import shows the correct order when you need both Bootstrap and custom CSS
The
user
constant is currently hardcoded but would typically come from:Authentication context
Redux store
API call
The commented out
Header
component shows where you might add additional layout elementsThis structure follows React Router v6 conventions, which differ from v5:
Switch
replaced withRoutes
Different pattern for route parameters
element
prop instead ofcomponent
🧭 4. EmployeeList.tsx
(List Employees)
📄 File: src/components/EmployeeList.tsx
Key Features :
State Management: Tracks employees, loading states, errors, and pagination
API Integration: Uses Axios for CRUD operations with error handling
Search Functionality: Implements debounced search to filter employees efficiently
Pagination: Splits data into pages with ReactPaginate
UI Components: Includes loading spinners, error messages, and empty states
Type Safety: Uses TypeScript interfaces for employee data
Responsive Design: Uses Bootstrap classes for responsive layout
// Import necessary React hooks and librariesimport React, { useEffect, useState, useCallback } from 'react'; // Core React functionalityimport axios from 'axios'; // For making HTTP requestsimport { Link } from 'react-router-dom'; // For client-side navigationimport { debounce } from 'lodash'; // Utility for debouncing search inputimport ReactPaginate from 'react-paginate'; // Pagination componentimport { Employee } from '../models/Employee'; // Type definition for Employee
// API endpoint configurationconst API_URL = "http://localhost:9090/api/employees"; // Base URL for employee API
// Main component definitionconst EmployeeList: React.FC = () => { // State management for various component needs const [employees, setEmployees] = useState<Employee[]>([]); // Stores all employees from API const [filteredEmployees, setFilteredEmployees] = useState<Employee[]>([]); // Stores filtered employees const [search, setSearch] = useState(""); // Current search term const [pageNumber, setPageNumber] = useState(0); // Current page number for pagination const employeesPerPage = 5; // Number of employees to show per page const [isLoading, setIsLoading] = useState(true); // Loading state flag const [error, setError] = useState<string | null>(null); // Error message storage
// Memoized function to fetch employees from API const fetchEmployees = useCallback(async () => { try { setIsLoading(true); // Set loading state const response = await axios.get<Employee[]>(API_URL); // API call setEmployees(response.data); // Store all employees setFilteredEmployees(response.data); // Initialize filtered list setError(null); // Clear any previous errors } catch (err) { setError("Failed to fetch employees. Please try again later."); console.error("Error fetching employees:", err); // Log error } finally { setIsLoading(false); // Always turn off loading state } }, []); // Empty dependency array means this is created once
// Effect hook to fetch employees when component mounts useEffect(() => { fetchEmployees(); // Call the fetch function }, [fetchEmployees]); // Only re-run if fetchEmployees changes
// Debounced search handler with memoization const handleSearchChange = useCallback( debounce((value: string) => { setSearch(value); // Update search term setFilteredEmployees( value ? employees.filter(emp => // Filter employees if search term exists emp.empName.toLowerCase().includes(value.toLowerCase()) || // Name match emp.empPhoneNumber.includes(value) // Phone match : employees // Return all employees if no search term ); setPageNumber(0); // Reset to first page when searching }, 300), // 300ms debounce delay [employees] // Recreate when employees list changes );
// Function to toggle employee active status const handleToggle = async (empId: number, currentStatus: boolean) => { try { await axios.put(`${API_URL}/disable/${empId}/${!currentStatus}`); // Toggle status fetchEmployees(); // Refresh list } catch (err) { setError("Failed to update employee status"); // Set error console.error("Error updating status:", err); // Log error } };
// Pagination calculations const pagesVisited = pageNumber * employeesPerPage; // Current page offset const displayEmployees = filteredEmployees.slice(pagesVisited, pagesVisited + employeesPerPage); // Employees to show const pageCount = Math.ceil(filteredEmployees.length / employeesPerPage); // Total pages needed const changePage = ({ selected }: { selected: number }) => setPageNumber(selected); // Page change handler
// Component rendering return ( <div className="container-fluid mt-4"> <div className="card shadow p-4"> {/* Error display */} {error && ( <div className="alert alert-danger"> {error} <button className="btn btn-sm btn-light ms-3" onClick={fetchEmployees} > Retry </button> </div> )}
{/* Search and add employee section */} <div className="d-flex justify-content-between align-items-center mb-3"> <div className="input-group w-50"> <span className="input-group-text"> <i className="bi bi-search"></i> {/* Search icon */} </span> <input type="text" className="form-control" placeholder="Search employees..." onChange={(e) => handleSearchChange(e.target.value)} // Trigger search disabled={isLoading} // Disable during loading /> </div>
{/* Add employee button */} <Link to="/add" className="btn btn-success"> <i className="bi bi-person-plus"></i> Add Employee </Link> </div>
{/* Loading state */} {isLoading ? ( <div className="text-center my-5"> <div className="spinner-border text-primary" role="status"> <span className="visually-hidden">Loading...</span> </div> </div> ) : ( <> {/* Employee table */} <div className="table-responsive"> <table className="table table-striped table-hover"> <thead className="table-dark"> <tr> <th>Name</th> <th>Designation</th> <th>Date of Join</th> <th>Contact</th> <th>Department</th> <th>Status</th> <th>Actions</th> </tr> </thead> <tbody> {/* Map through employees to display rows */} {displayEmployees.map((emp) => ( <tr key={emp.empId}> <td>{emp.empName}</td> <td>{emp.designation}</td> <td>{new Date(emp.dateOfJoining).toLocaleDateString()}</td> <td>{emp.empPhoneNumber}</td> <td>{emp.department?.deptName}</td> <td> {/* Status toggle switch */} <div className="form-check form-switch"> <input className="form-check-input" type="checkbox" checked={emp.isActive} onChange={() => handleToggle(emp.empId, emp.isActive)} style={{ backgroundColor: emp.isActive ? "#28a745" : "#dc3545", width: "2.5rem", height: "1.4rem", }} /> </div> </td> <td> {/* Edit button */} <Link to={`/edit/${emp.empId}`} className="btn btn-primary btn-sm me-2" > <i className="bi bi-pencil"></i> Edit </Link> </td> </tr> ))} </tbody> </table> </div>
{/* Empty state message */} {filteredEmployees.length === 0 && !isLoading && ( <div className="text-center my-4 text-muted"> No employees found {search && `matching "${search}"`} </div> )}
{/* Pagination component */} <ReactPaginate previousLabel={<i className="bi bi-chevron-left"></i>} nextLabel={<i className="bi bi-chevron-right"></i>} pageCount={pageCount} onPageChange={changePage} containerClassName="pagination justify-content-center mt-4" activeClassName="active" pageClassName="page-item" pageLinkClassName="page-link" previousClassName="page-item" nextClassName="page-item" previousLinkClassName="page-link" nextLinkClassName="page-link" breakLabel="..." forcePage={pageNumber} /> </> )} </div> </div> );};
export default EmployeeList;
📦 5. Models (for strong typing)
📄 File: src/models/Employee.ts
import { Department } from "./Department";
export interface Employee { empId: number; empName: string; designation: string; dateOfJoining: string; salary: number; empPhoneNumber: string; deptId: number; isActive: boolean; department?: Department;}
📄 File: src/models/Department.ts
export interface Department { deptId: number; deptName: string;}
No comments:
Post a Comment