Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"ag-grid-community": "^35.0.1",
"ag-grid-enterprise": "^35.0.1",
"ag-grid-react": "^35.0.1",
"axios": "^1.13.4",
"clsx": "^2.1.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.18"
Expand Down
27 changes: 6 additions & 21 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
import './index.css'

import './index.css';
import { UsersGrid } from './features/users-grid/components/UsersGrid';
function App() {

return (
<>
<div className="bg-green-500 text-white p-4 mt-4 rounded-lg shadow-md">
<p className="text-lg">User Management System</p>
</div>
<div className="bg-red-500 text-white p-4 mt-4 rounded-lg shadow-md">
<button className="bg-white text-red-500 px-4 py-2 rounded hover:bg-gray-100">Login</button>
</div>
<div className="bg-yellow-500 text-black p-4 mt-4 rounded-lg shadow-md">
<input className="border border-gray-300 p-2 rounded w-full" placeholder="Enter username" />
</div>
<div className="bg-purple-500 text-white p-4 mt-4 rounded-lg shadow-md">
<ul className="list-disc list-inside">
<li>Manage Users</li>
<li>View Profiles</li>
<li>Edit Settings</li>
</ul>
</div>
</>
)
<div className="flex flex-col gap-8 p-8">
<UsersGrid />
</div>
);
}

export default App
25 changes: 25 additions & 0 deletions src/features/users-grid/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import clsx from "clsx";

type ButtonProps = {
label: string;
onClick: () => void;
isActive?: boolean;
};

export function Button({ label, onClick, isActive }: ButtonProps) {
const base = "px-4 py-2 text-sm rounded transition font-medium";

return (
<button
onClick={onClick}
className={clsx(
base,
isActive
? "bg-green-600 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300",
)}
>
{label}
</button>
);
}
79 changes: 79 additions & 0 deletions src/features/users-grid/components/UsersGrid.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";

import { UsersGrid } from "./UsersGrid";
import type { UserProps } from "../types/users.types";

const mockUsers: UserProps[] = [
{
id: 1,
firstName: "John",
lastName: "Doe",
maidenName: "Smith",
age: 30,
gender: "male",
email: "john@test.com",
phone: "1234567890",
username: "john doe",
birthDate: "1994-01-01",
bloodGroup: "O+",
height: 180,
weight: 75,
eyeColor: "Blue",
isActive: true,
},
];

vi.mock("../hooks/useUsersGrid", () => ({
useUsersApi: () => ({
users: mockUsers,
isLoading: false,
}),
}));

vi.mock("ag-grid-react", () => ({
AgGridReact: () => (
<div>
<div>First Name</div>
<div>John</div>
</div>
),
}));

const renderComponent = () => render(<UsersGrid />);

describe("UsersGrid", () => {
it("renders the grid heading", () => {
renderComponent();

expect(
screen.getByRole("heading", {
name: /user management grid/i,
}),
).toBeInTheDocument();
});

it("renders column headers", () => {
renderComponent();

expect(screen.getByText("First Name")).toBeInTheDocument();
});

it("renders user data in the grid", () => {
renderComponent();

expect(screen.getByText("John")).toBeInTheDocument();
});

it("renders filter toggle buttons", () => {
renderComponent();

expect(
screen.getByRole("button", { name: /show filters/i }),
).toBeInTheDocument();

expect(
screen.getByRole("button", { name: /hide filters/i }),
).toBeInTheDocument();
});
});
75 changes: 75 additions & 0 deletions src/features/users-grid/components/UsersGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState } from "react";
import { AgGridReact } from "ag-grid-react";
import { ModuleRegistry, AllCommunityModule } from "ag-grid-community";
import { SetFilterModule } from "ag-grid-enterprise";
import { useUsersApi } from "../hooks/useUsersGrid";
import type { UserProps } from "../types/users.types";

import {
DEFAULT_PAGINATION_PAGE_SIZE,
PAGINATION_PAGE_SIZE_OPTIONS,
} from "../constants/users.constants";

import "ag-grid-community/styles/ag-theme-alpine.css";
import { getUserColumnDefs, getDefaultColDef } from "../utils/userColumnDefs";
import { Button } from "./Button";

ModuleRegistry.registerModules([AllCommunityModule, SetFilterModule]);

export function UsersGrid() {
const { users: userData, isLoading } = useUsersApi();
const [showFilters, setShowFilters] = useState(false);

const columnDefs = getUserColumnDefs(showFilters);
const defaultColDef = getDefaultColDef(showFilters);

const filterButtons = [
{
label: "Show Filters",
isActive: showFilters,
onClick: () => setShowFilters(true),
},
{
label: "Hide Filters",
isActive: !showFilters,
onClick: () => setShowFilters(false),
},
];

return (
<div className="h-screen flex flex-col bg-gray-50">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 bg-white border-b shadow-sm">
<h2 className="text-xl font-bold tracking-wide text-gray-800">
👥 User Management Grid
</h2>

<div className="flex gap-3">
{filterButtons.map(({ label, onClick, isActive }) => (
<Button
key={label}
label={label}
onClick={onClick}
isActive={isActive}
/>
))}
</div>
</div>

{/* Grid Wrapper */}
<div className="flex-1 p-4 overflow-x-auto">
<AgGridReact<UserProps>
className="ag-theme-alpine min-w-300 rounded-lg border shadow-sm bg-violet-500"
containerStyle={{ height: 600 }}
rowData={userData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
loading={isLoading}
pagination
paginationPageSize={DEFAULT_PAGINATION_PAGE_SIZE}
paginationPageSizeSelector={PAGINATION_PAGE_SIZE_OPTIONS}
/>
</div>
</div>
);
}
13 changes: 13 additions & 0 deletions src/features/users-grid/constants/users.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const USERS_API_BASE_URL = 'https://dummyjson.com';

export const DEFAULT_PAGINATION_PAGE_SIZE = 10;
export const PAGINATION_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];

export const Gender = {
Male: 'male',
Female: 'female',
} as const;

export type Gender = (typeof Gender)[keyof typeof Gender];

export const GENDER_OPTIONS = [Gender.Male, Gender.Female];
55 changes: 55 additions & 0 deletions src/features/users-grid/hooks/useUsersGrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import type { UserProps } from '../types/users.types';
import { USERS_API_BASE_URL } from '../constants/users.constants';
import axios from 'axios';

export function useUsersApi() {
const [users, setUsers] = useState<UserProps[]>([]);
const [isLoading, setIsLoading] = useState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Add a small unit test for the error path (e.g., axios rejects) to ensure isLoading resets and the new error state is set, preventing regressions to “empty grid with no signal.”

useEffect(function () {
let isMounted = true;
const abortController = new AbortController();

async function fetchUsersOnMount() {
setIsLoading(true);

try {
Comment on lines +14 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Errors from the request are currently swallowed; add a catch (and ideally request cancellation) so failures are observable and don’t silently render an empty grid.

try {
  ...
} catch (error) {
  if (isMounted) console.error('Failed to load users', error);
}

const response = await axios.get<{ users: UserProps[] }>(`${USERS_API_BASE_URL}/users`, {
signal: abortController.signal,
});

if (isMounted) {
const mappedUsers = response.data.users.map(function (user: UserProps) {
return {
...user,
isActive: user.age < 50,
};
});

setUsers(mappedUsers);
}
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Expose an error state (and type the hook return) so fetch failures don’t silently render an empty grid.

} catch (error: unknown) {
  if (isMounted) {
    setError(error);
  }
}

if (isMounted && !axios.isCancel(error)) {
console.error('Failed to load users', error);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
}

fetchUsersOnMount();

return function () {
isMounted = false;
abortController.abort();
};
}, []);

return {
users,
isLoading,
};
}
Loading