Skip to content

Frontend Challenge Implementation#52

Open
AllanMicuanski wants to merge 42 commits into
sizebay:mainfrom
AllanMicuanski:main
Open

Frontend Challenge Implementation#52
AllanMicuanski wants to merge 42 commits into
sizebay:mainfrom
AllanMicuanski:main

Conversation

@AllanMicuanski

Copy link
Copy Markdown

Implementation of the proposed frontend challenge.

Looking forward to your feedback.

@AllanMicuanski
AllanMicuanski requested a review from breno5g March 6, 2026 18:50
@AllanMicuanski

AllanMicuanski commented Mar 7, 2026

Copy link
Copy Markdown
Author

Thank you for the feedback and for the time dedicated to the review. I carefully analyzed all the comments and implemented the adjustments accordingly.

✅ Summary of Changes
Removed the unused Button component (YAGNI principle)
Fixed UX interaction issues: cursor feedback, filter toggle behavior, border alignment, and edit consistency
Refactored ConfettiAnimation Storybook configuration: fullscreen layout, play function, and autodocs improvements
Introduced a design token strategy using clsx and centralized semantic constants for colors, spacing, typography, and animation — eliminating magic values across components
Split TaskItem into smaller components: TaskActions and TaskTooltip
Refactored tests to focus on user-visible behavior rather than implementation details

🧪 Testing Strategy
Unit tests were refactored with Jest + React Testing Library, removing CSS class assertions and low-value mocks in favor of observable behavior and accessibility. 7 E2E tests were added with Playwright, covering the main user flows: create, complete, filter, search, edit, delete, input validation, and persistence.

AllanMicuanski and others added 9 commits March 7, 2026 15:33
- Install clsx library (v2.1.1) for proper className handling
- Refactor TaskItem: use clsx for container and span dynamic classes
- Refactor TaskForm: use clsx for button and svg dynamic opacity
- Refactor Input: use clsx for conditional error styling
- Eliminates extra spaces from template literal concatenation
- Improves code readability and maintainability

Addresses tech lead's comment: "use clsx or cn (shadcn pattern)" to avoid
fragile className concatenation with empty string defaults
- Add --color-gray-50 token to design system
- Replace hardcoded hex colors with semantic Tailwind tokens across components:
  * TaskItem: border-primary, text-text-primary, bg-danger, bg-success, bg-gray-medium, bg-gray-50
  * TaskForm: bg-gray-lighter, border-gray-light, text-text-primary, bg-primary
  * FilterBar: bg-white, border-gray-light, focus:border-primary, text-primary, bg-gray-lighter
  * ProgressBar: bg-border, bg-success
  * ErrorBoundary: bg-background, text-danger, bg-primary, bg-gray-lighter
  * SearchBar: border-gray-light, focus:border-primary
  * TaskListSkeleton: bg-gray-50, border-gray-light, bg-border
  * Input: clsx refactoring for conditional styles
- Standardize SVG icons in TaskItem:
  * Use consistent filled circle structure in both editing and selected modes
  * Match stroke colors to button backgrounds (#E34F4F for danger, #5DE290 for success)
- Improve maintainability: centralized color management in index.css @theme block
- Change layout from 'centered' to 'fullscreen' for proper viewport coverage
- Add play function to Visible story documenting auto-play behavior
- Move detailed description to component JSDoc for autodocs integration
- Enhance argTypes with control descriptions
- Added clsx to technologies section
- Highlighted design tokens implementation
- Updated TaskForm icon color to match primary brand color (#4DA6B3)
App.tsx: remove useState callback anti-pattern
TaskList: use useRef instead of callback for deselection
SearchBar: delete unused component (-73 lines)
Header: fix month to uppercase

Addresses PR sizebay#52 feedback point #1
- Extract editing logic to useTaskEdit hook (56 lines)
- Create TaskTooltip component for edit hint (32 lines)
- Create TaskActions component for delete/complete buttons (84 lines)
- Reduce TaskItem from 269 to 135 lines (-50%)
- Improve testability and maintainability

Addresses PR sizebay#52 feedback on component complexity
- Introduce Playwright E2E tests covering main user flows
- Remove implementation-detail assertions from unit tests
- Remove redundant and mocked integration tests
- Focus unit tests on behavior verification
- Configure jest to ignore E2E paths
- Adjust coverage thresholds to behavior-oriented strategy
@AllanMicuanski
AllanMicuanski requested a review from breno5g March 7, 2026 23:24
Comment thread e2e/user-flows.spec.ts

test.describe("Todo App - User Flows", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given that we have a before-each statement, passing this in each test is redundant.

Comment thread e2e/user-flows.spec.ts
await expect(progressBar).toHaveAttribute("aria-valuenow", "0");

// Complete task
await page.getByText("Buy bread").click();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of using mutable attributes, tests should be guided by elements or, in more specific cases, data attributes.

Comment thread e2e/user-flows.spec.ts

/**
* Helper to type text into an input - simulates real user typing
* Playwright's fill() doesn't trigger React onChange correctly

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a strong and potentially incorrect statement. Playwright's fill() triggers input events and works correctly with React in modern versions. The helper may be unnecessary and adds an artificial 30ms delay per character, unnecessarily slowing down the suite. It should be documented with a reference or issue.

Comment thread e2e/user-flows.spec.ts
Comment on lines +19 to +21
await page.goto("/");
await page.evaluate(() => localStorage.clear());
await page.reload();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given that we already have a goto, the reload becomes redundant in this scenario.
By changing the order of the calls, we can eliminate one of them.

Comment thread e2e/user-flows.spec.ts
await addButton.click();

// Search
await typeIntoInput(page.getByPlaceholder("search items"), "buy");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, a problem with mutable selectors.

If the application were in another language, for example, this element would not appear in this way.

Comment thread e2e/user-flows.spec.ts
await page.getByLabel("Delete task: To delete").click();

// Task gone
await expect(page.getByText("To delete")).not.toBeVisible();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the element is in the DOM but hidden (e.g., display: none), the test passes even if the item hasn't been removed. A more precise assertion would be not.toBeAttached() or toHaveCount(0) to guarantee actual removal from the DOM.

Comment thread src/App.tsx
import { useTaskContext } from "./hooks/useTaskContext";

function TodoApp() {
const { tasks, addTask, toggleTask, updateTask, deleteTask, filter, setFilter, searchValue, setSearchValue, filteredTasks, isLoading } = useTaskContext();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TodoApp is consuming 11 context values ​​at once. This violates the single responsibility principle—the component knows everything. Ideally, each child component would only consume the slice of context it needs. This also ensures that any change to the context re-renders the entire TodoApp, even if the affected child is only FilterBar.

Comment thread src/App.tsx
function TodoApp() {
const { tasks, addTask, toggleTask, updateTask, deleteTask, filter, setFilter, searchValue, setSearchValue, filteredTasks, isLoading } = useTaskContext();

const completedCount = tasks.filter(task => task.completed).length;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This value is calculated in TodoApp but should be in useTaskContext along with filteredTasks. filteredTasks is already a derived value in the context — completedCount should have the same treatment to avoid scattered logic. It also iterates tasks on a third pass in addition to those that the context already does.
And we also have the problem of generating new values ​​with each render.

Comment thread src/App.tsx
return (
<div className="min-h-screen bg-[#555555] flex items-center justify-center p-4">
<Modal>
<div className="w-[309px] h-auto md:w-[680px] md:h-[495px]">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The values ​​309px and 680px appear twice, coupling the inner and outer layouts. If the breakpoint changes, it needs to be altered in two places. These should be extracted as CSS variables, Tailwind tokens, or a layout wrapper component.

Comment thread src/App.tsx
searchValue={searchValue}
onSearchChange={setSearchValue}
/>
<div className="w-[309px] h-auto md:w-[680px] md:h-[280px]">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And, speaking purely from a development standpoint, defining the size of the children separately is a terrible practice, as it forces you to do all the responsiveness manually, whereas with the flex display and relative measurements, this is resolved automatically.

Comment thread src/App.tsx
const completedCount = tasks.filter(task => task.completed).length;

return (
<div className="min-h-screen bg-[#555555] flex items-center justify-center p-4">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Arbitrary color outside the design system. It should be a token (e.g., bg-surface or bg-zinc-600), especially if the project has a theme.

Comment thread src/App.tsx
}

export default App;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We have a blank line at the end of the file, which shouldn't happen with a properly configured linter.

key={task.id}
task={task}
isSelected={selectedTaskId === task.id}
onSelect={() => setSelectedTaskId(selectedTaskId === task.id ? null : task.id)}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A new function is created for each item, on each render. Inside a .map(), the impact is multiplied by the number of tasks. If TaskItem is memoized (or becomes memoized), this breaks memoization completely. It should be a stable handler, e.g., useCallback with the id as an argument.

Comment on lines +39 to +40
const prevFilterRef = useRef<TaskFilter | undefined>(currentFilter);
const prevSearchRef = useRef<string | undefined>(searchValue);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This pattern manually simulates prevProps. The problem: useEffect already reacts to changes in dependencies—the internal conditional check and refs are redundant. The same effect could be achieved with:

useEffect(() => {
  setSelectedTaskId(null);
}, [currentFilter, searchValue]);


// Mensagem para busca sem resultados
if (tasks.length === 0 && searchValue && searchValue.trim()) {
return (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Three if blocks with repetitive JSX and duplicated dimensions (w-[309px] md:w-[680px] h-[19px]). Besides the duplication, the logic for which message to display is mixed with the JSX rendering. It should be extracted into an EmptyState component or at least a helper function.

Lines 52-93

// Mensagem para busca sem resultados
if (tasks.length === 0 && searchValue && searchValue.trim()) {
return (
<div className="w-[309px] md:w-[680px] h-[19px] text-left">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The values ​​309px, 680px, 19px, 14px, #848484, and #3d8a94 appear in multiple places in the file and likely in other files in the project. Without tokens, any visual adjustment becomes a manual find-replace, risking inconsistency.

}
}, [currentFilter, searchValue]);

// Mensagem para busca sem resultados

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The rest of the code (JSX, strings, props) is in English. Mixed-language comments make maintenance difficult for mixed teams and are inconsistent with the file standard.

}

if (tasks.length === 0) {
return <div className="w-[309px] md:w-[680px] h-[19px]"></div>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

An empty element with a fixed height to "reserve space" is fragile and lacks semantics. If the layout changes, this phantom spacing is difficult to track. It should be null or a true empty state component.

Comment thread playwright.config.ts
/* Shared settings for all the projects below */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5174",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here, a port is set, but in the project settings it's not fixed, so on my machine, for example, it starts on a port with the lowest value; therefore, the tests will not run.

@breno5g breno5g left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  1. When loading the page for the first time (ctrl + shift + r) we can see all the text on the page flashing with a different font.
  2. The src/constants/theme.ts file defines COLORS, DIMENSIONS, and ANIMATION as TypeScript constants. However, the components use hardcoded values ​​in Tailwind (text-[#848484], w-[309px], bg-[#555555]). The two sources are not connected—any visual adjustments need to be made in two places, with a real risk of inconsistency. The tokens exist but are not used.
  3. The selection state of which task is active (selectedTaskId) resides in TaskList and is propagated to each TaskItem via isSelected + onSelect. This means that TaskList re-renders all items when any one is selected. The selection state is local to an item—it should be managed with an approach that isolates the re-rendering (e.g., state in the TaskItem itself or a dedicated selection Context).
  4. In src/components/TaskItem/index.tsx, the tooltip position is calculated via getBoundingClientRect() and stored in state. This breaks down into: list scroll (absolute vs. relative coordinates to scroll), browser zoom, and any reflow caused by resize. The use of portal is correct, but the position calculation should use IntersectionObserver or CSS anchoring instead of absolute coordinates.
  5. The function uses Date.now() (1ms resolution) + 7 random characters. In quick sequential creations (e.g., pasting multiple tasks, automation), collisions are possible. For a local app with localStorage, the risk is low, but the function lacks documentation of its limitations—and if the logic migrates to a backend, duplicate IDs become a data integrity problem. crypto.randomUUID() is available in all modern browsers and solves this at no cost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test-only PRs that were opened by Developers who want to get CRed by Sizebay.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants