Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
f384661
refactor: optimize GitHub Actions workflows with composite actions an…
codegen-sh[bot] Aug 6, 2025
9d7e070
Fix lockfile synchronization issue
codegen-sh[bot] Aug 6, 2025
f101f1c
fix: resolve linting errors across packages
codegen-sh[bot] Aug 6, 2025
ee570f6
ci(cache): make Turborepo cache key commit-specific and keep lockfile…
codegen-sh[bot] Aug 6, 2025
2b256ca
fix: remove invalid Tailwind 'fixed' variant from utility classes
codegen-sh[bot] Aug 6, 2025
b2b1c85
fix: resolve TypeScript errors in form components
codegen-sh[bot] Aug 6, 2025
bbd89d0
fix: resolve monorepo TypeScript configuration issues
codegen-sh[bot] Aug 6, 2025
1032007
Add test files and fix FormError components
codegen-sh[bot] Aug 6, 2025
f4c3484
Merge main into codegen-bot/refactor-github-actions-workflows-1754516…
codegen-sh[bot] Aug 8, 2025
25ea083
Resolve merge markers in root.tsx, keep TodoProvider and MetaFunction…
codegen-sh[bot] Aug 8, 2025
080a42b
Fix lint issues: remove unused type import in todo-context, replace e…
codegen-sh[bot] Aug 8, 2025
09af38e
chore: address biome lint warnings\n- Hoist regex literals in add-tod…
codegen-sh[bot] Aug 8, 2025
1bb8154
test(utils): simplify boolean logic in cn.test to satisfy biome rule\…
codegen-sh[bot] Aug 8, 2025
5279de1
test(utils): remove void expressions; use no-op functions with explic…
codegen-sh[bot] Aug 8, 2025
5985235
test(todo-context): satisfy biome rules (button type, no-op console.e…
codegen-sh[bot] Aug 8, 2025
01f7400
test(ui): configure vitest + jest-dom types so TS matchers resolve\n-…
codegen-sh[bot] Aug 8, 2025
3d76fe0
fix(todo-app): add required FormError name prop to satisfy types\n\nC…
codegen-sh[bot] Aug 8, 2025
af9cb46
test(todo-app): wrap AddTodo tests with MemoryRouter to satisfy useHr…
codegen-sh[bot] Aug 8, 2025
ca43444
test(todo-app): render AddTodo via createMemoryRouter/RouterProvider …
codegen-sh[bot] Aug 8, 2025
ca3ebb3
feat(todos): persist todos in localStorage with storage utils and pro…
codegen-sh[bot] Aug 8, 2025
74ea542
test(todo-app): use MemoryRouter from react-router-dom to provide Rou…
codegen-sh[bot] Aug 8, 2025
e59dd38
test(todo-app): stabilize initial todos to all active to make clearCo…
codegen-sh[bot] Aug 8, 2025
37dd5bc
fix(todo-app): prevent default on AddTodo form submit to allow client…
codegen-sh[bot] Aug 8, 2025
e2bde01
test(todo-app): make AddTodo testable without data router by handling…
codegen-sh[bot] Aug 8, 2025
14addb0
chore: merge latest main into actions refactor working branch and res…
codegen-sh[bot] Aug 8, 2025
b4a12cc
chore: fix lint issues failing CI (biome, tests, and types)\n\n- Hois…
codegen-sh[bot] Aug 8, 2025
ec3dbc5
chore(tsconfig): add per-package tsconfig to avoid cross-package path…
codegen-sh[bot] Aug 8, 2025
24a407c
chore(utils): allow empty test suite in CI to unblock workflow\n\nCo-…
codegen-sh[bot] Aug 8, 2025
57bd96c
chore(ui): allow empty test suite in CI to unblock workflow\n\nCo-aut…
codegen-sh[bot] Aug 8, 2025
f28d317
fix(tests): align initial seed state with test expectations (only one…
codegen-sh[bot] Aug 8, 2025
165c756
fix(css): replace invalid Tailwind variant chain with valid utilities…
codegen-sh[bot] Aug 8, 2025
e6a7c59
Merge pull request #8 from lambda-curry/codegen-bot/merge-main-into-a…
jaruesink Aug 9, 2025
60bd6cf
Merge branch 'main' of github.com:lambda-curry/react-router-starter i…
jaruesink Aug 9, 2025
4b16a9a
Add optional validate guard to loadFromStorage and unit tests\n\nCo-a…
codegen-sh[bot] Aug 9, 2025
d0927af
fix(css): update import path for @lambdacurry/forms to use the dist d…
jaruesink Aug 9, 2025
1daac84
Merge branch 'main' of github.com:lambda-curry/react-router-starter i…
jaruesink Aug 9, 2025
b034ea2
Merge pull request #6 from lambda-curry/codegen-bot/refactor-github-a…
jaruesink Aug 9, 2025
a5386c5
feat(todos): persist todos in localStorage with storage utils and pro…
codegen-sh[bot] Aug 8, 2025
fdeb1c8
test(storage): add storage utils tests and optional validation; exten…
codegen-sh[bot] Aug 9, 2025
e70bd83
Revert seed: set second initial todo to completed; make tests indepen…
Aug 9, 2025
ff9f73a
chore: fix lint issues failing CI (biome, tests, and types)\n\n- Hois…
codegen-sh[bot] Aug 8, 2025
fd3755f
chore(tsconfig): add per-package tsconfig to avoid cross-package path…
codegen-sh[bot] Aug 8, 2025
eff66cd
chore(utils): allow empty test suite in CI to unblock workflow\n\nCo-…
codegen-sh[bot] Aug 8, 2025
62a178d
chore(ui): allow empty test suite in CI to unblock workflow\n\nCo-aut…
codegen-sh[bot] Aug 8, 2025
6d7b289
fix(tests): align initial seed state with test expectations (only one…
codegen-sh[bot] Aug 8, 2025
b97e8cc
fix(css): replace invalid Tailwind variant chain with valid utilities…
codegen-sh[bot] Aug 8, 2025
cc1db4f
fix(css): update import path for @lambdacurry/forms to use the dist d…
jaruesink Aug 9, 2025
406bfd2
merge conflicts
jaruesink Aug 9, 2025
25f4472
Polish: clarify mock comment in AddTodo tests; re-run tests (still gr…
Aug 9, 2025
3a52810
Merge branch 'codegen-bot/persist-todos-localstorage-STA-2-1754688724…
jaruesink Aug 9, 2025
eb25867
fix(tests): enhance AddTodo component tests with stateful mocks and r…
jaruesink Aug 9, 2025
bdf695d
feat(tests): add watch mode for Vitest and enhance test configurations
jaruesink Aug 9, 2025
b12ecc7
Merge branch 'codegen-bot/persist-todos-localstorage-STA-2-1754688724…
jaruesink Aug 9, 2025
98f8b92
merge conflicts
jaruesink Aug 9, 2025
501a061
refactor(tests): enhance todo-context tests with improved localStorag…
jaruesink Aug 9, 2025
c7b40a2
fix typecheck
jaruesink Aug 9, 2025
9a7b467
refactor(todos): streamline todo context reducer and improve formatting
jaruesink Aug 9, 2025
ca5d860
refactor(storage): simplify loadFromStorage function signatures
jaruesink Aug 9, 2025
6cfffe6
Remove AddTodo component tests and update test command to allow passi…
jaruesink Aug 9, 2025
e67690e
merge conflicts
jaruesink Aug 9, 2025
db102d2
Merge pull request #13 from lambda-curry/codegen/sta-8-correctness-re…
jaruesink Aug 9, 2025
09fa3ce
resolve: merge conflict in todo-context tests; align imports and util…
jaruesink Aug 9, 2025
ab920c1
test: seed hydration test via saveToStorage mock instead of direct lo…
jaruesink Aug 9, 2025
8b2d28b
chore(todo-app): update tsconfig to include testing types and remove …
jaruesink Aug 9, 2025
073512b
Merge pull request #12 from lambda-curry/codegen/sta-10-testing-add-s…
jaruesink Aug 9, 2025
926fad4
Merge branch 'codegen-bot/persist-todos-localstorage-STA-2-1754688724…
jaruesink Aug 9, 2025
2cfaab9
Merge pull request #11 from lambda-curry/codegen/sta-7-major-add-runt…
jaruesink Aug 9, 2025
721508f
refactor(AddTodo): streamline form submission and remove client-only …
jaruesink Aug 9, 2025
da4c5f9
fix(AddTodo): clarify form submission logic and update tests to use f…
jaruesink Aug 9, 2025
79e7e20
fix test
jaruesink Aug 9, 2025
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
8 changes: 1 addition & 7 deletions .github/composite/bun-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ inputs:
description: 'Bun version to use'
required: false
default: '1.2.19'
cache-key:
description: 'Cache key for dependencies'
required: false
default: ''

runs:
using: 'composite'
Expand All @@ -21,14 +17,12 @@ runs:

- name: Cache dependencies
uses: actions/cache@v4
if: inputs.cache-key != ''
with:
path: ~/.bun
key: ${{ inputs.cache-key }}
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Cache key likely wrong: Bun’s lockfile is “bun.lockb”, not “bun.lock”.

Using bun.lock here will cause cache misses. Switch to bun.lockb (or hash both for safety).

-        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
+        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lockb') }}

Optional: hash both

-        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
+        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lockb', '**/bun.lock') }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lockb') }}
🤖 Prompt for AI Agents
In .github/composite/bun-install/action.yml at line 22, the cache key is using
'bun.lock' which is incorrect for Bun; it should use 'bun.lockb' instead. Update
the hashFiles argument to reference 'bun.lockb' to prevent cache misses.
Optionally, you can hash both 'bun.lock' and 'bun.lockb' files for safety by
including both patterns in the hashFiles function.

restore-keys: |
${{ runner.os }}-deps-
- name: Install dependencies
shell: bash
run: bun install --frozen-lockfile

22 changes: 5 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,17 @@ jobs:
with:
fetch-depth: 0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.2.19'

- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.bun
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-deps-

- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Bun and install dependencies
uses: ./.github/composite/bun-install

- name: Cache Turbo
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-

- name: Run Turbo lint
Expand All @@ -68,4 +57,3 @@ jobs:
echo "✅ Tests passed" >> $GITHUB_STEP_SUMMARY
echo "✅ All checks completed with Turbo caching" >> $GITHUB_STEP_SUMMARY
echo "✅ Ready for deployment" >> $GITHUB_STEP_SUMMARY

22 changes: 5 additions & 17 deletions .github/workflows/pr-quality-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- 'bun.lock'
- 'turbo.json'
- 'biome.json'
- '.github/workflows/**'
- '!**/*.md'
- '!**/*.txt'

Expand All @@ -33,28 +34,16 @@ jobs:
with:
fetch-depth: 0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.2.19'

- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.bun
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-deps-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Bun and install dependencies
uses: ./.github/composite/bun-install

- name: Cache Turbo
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ github.sha }}
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-
Comment on lines +44 to 48
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm bun.lockb presence across the monorepo for cache key effectiveness

Turbo cache keys now hash **/bun.lockb, but the workflow triggers still list bun.lock (Line 12). If the repo uses bun.lockb (Bun’s default), consider updating triggers and verify that lockfiles exist so cache keys are stable.

Run to verify presence and consistency:


🏁 Script executed:

#!/bin/bash
# List Bun lockfiles and their paths
fd -HI --regex 'bun\.lockb?$' | sort

# Show which workflows reference bun.lock vs bun.lockb
rg -n "bun\.lockb?" -A 2 -B 2 .github/workflows

Length of output: 1631


Use bun.lock for Turbo cache keys in workflows

We only have bun.lock in the repo (no bun.lockb), so the current hashFiles('**/bun.lockb') calls will never match anything and the cache key will always fall back to the less specific prefixes. Update both workflows to hash the actual lockfile instead:

• .github/workflows/pr-quality-checks.yml (lines 44–48)
• .github/workflows/ci.yml (lines 33–37)

Suggested diff for each:

- key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
+ key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-${{ github.sha }}

- restore-keys: |
-   ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
+ restore-keys: |
+   ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-
    ${{ runner.os }}-turbo-${{ github.ref_name }}-
    ${{ runner.os }}-turbo-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-
🤖 Prompt for AI Agents
In .github/workflows/pr-quality-checks.yml around lines 44 to 48, the cache key
uses hashFiles('**/bun.lockb'), but the repo only contains bun.lock, so this
hash will never match. Update the hashFiles argument to '**/bun.lock' in all
occurrences to correctly hash the existing lockfile and improve cache key
specificity.

Expand All @@ -78,4 +67,3 @@ jobs:
echo "✅ TypeScript compilation passed" >> $GITHUB_STEP_SUMMARY
echo "✅ Tests passed" >> $GITHUB_STEP_SUMMARY
echo "✅ All checks completed with Turbo caching" >> $GITHUB_STEP_SUMMARY
180 changes: 152 additions & 28 deletions apps/todo-app/app/components/__tests__/add-todo.test.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,187 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AddTodo } from '../add-todo';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import type { ReactElement, ReactNode, ChangeEvent, FormEvent } from 'react';

// Create a stateful mock for the input field
let testInputValue = '';

// Mock lucide-react icons
vi.mock('lucide-react', () => ({
Plus: () => null
}));

// Mock the @lambdacurry/forms components
interface TextFieldProps {
name: string;
placeholder: string;
className: string;
}

vi.mock('@lambdacurry/forms', () => ({
TextField: ({ name, placeholder, className }: TextFieldProps) => (
<input
name={name}
placeholder={placeholder}
className={className}
type="text"
value={testInputValue}
onChange={e => {
testInputValue = e.target.value;
}}
/>
),
FormError: () => null
}));

interface ButtonProps {
children: ReactNode;
onClick: () => void;
type: 'button' | 'submit' | 'reset';
}

vi.mock('@lambdacurry/forms/ui', () => ({
Button: ({ children, onClick, type }: ButtonProps) => (
<button type={type} onClick={onClick}>
{children}
</button>
)
}));

// Mock the remix-hook-form module
interface RemixFormConfig {
submitHandlers?: {
onValid: (data: { text: string }) => void;
};
[key: string]: unknown;
}

vi.mock('remix-hook-form', () => {
let latestConfig: RemixFormConfig | undefined;
return {
RemixFormProvider: ({ children }: { children: ReactNode }) => children,
useRemixForm: (config: RemixFormConfig) => {
latestConfig = config;
return {
...config,
getValues: (_name: string) => testInputValue,
reset: vi.fn(() => {
testInputValue = '';
// Force re-render by dispatching a custom event
const inputs = document.querySelectorAll('input[name="text"]');
inputs.forEach(input => {
(input as HTMLInputElement).value = '';
});
}),
setValue: vi.fn((_name: string, value: string) => {
testInputValue = value;
}),
register: vi.fn((name: string) => ({
name,
onChange: (e: ChangeEvent<HTMLInputElement>) => {
testInputValue = e.target.value;
},
value: testInputValue
})),
handleSubmit: vi.fn((arg?: unknown) => {
// Support both usages:
// 1) onSubmit={methods.handleSubmit} → arg is the FormEvent
// 2) onSubmit={methods.handleSubmit(onValid)} → arg is the onValid callback
const isEvent = arg && typeof (arg as FormEvent).preventDefault === 'function';
if (isEvent) {
const e = arg as FormEvent;
e.preventDefault();
const onValid = latestConfig?.submitHandlers?.onValid;
if (onValid && testInputValue?.trim()) onValid({ text: testInputValue.trim() });
return undefined;
}
const maybeOnValid = arg as ((data: { text: string }) => void) | undefined;
return (e: FormEvent) => {
e.preventDefault();
const onValid = maybeOnValid || latestConfig?.submitHandlers?.onValid;
if (onValid && testInputValue?.trim()) onValid({ text: testInputValue.trim() });
};
}),
formState: { errors: {} },
watch: vi.fn((_name: string) => testInputValue)
};
}
};
});

function renderWithRouter(ui: ReactElement) {
const router = createMemoryRouter([{ path: '/', element: ui }], { initialEntries: ['/'] });
return render(<RouterProvider router={router} />);
}

// hoist regex literals to top-level to satisfy biome's useTopLevelRegex
const ADD_REGEX = /add/i;

describe('AddTodo', () => {
beforeEach(() => {
// Reset the test state before each test
testInputValue = '';
});

it('renders input and button', () => {
const mockOnAdd = vi.fn();
render(<AddTodo onAdd={mockOnAdd} />);
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);

expect(screen.getByPlaceholderText('Add a new todo...')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: ADD_REGEX })).toBeInTheDocument();
});

it('calls onAdd when form is submitted with text', () => {
const mockOnAdd = vi.fn();
render(<AddTodo onAdd={mockOnAdd} />);
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);

const input = screen.getByPlaceholderText('Add a new todo...');
const button = screen.getByRole('button', { name: /add/i });

const button = screen.getByRole('button', { name: ADD_REGEX });
const form = button.closest('form') as HTMLFormElement;

fireEvent.change(input, { target: { value: 'New todo' } });
fireEvent.click(button);
fireEvent.submit(form);

expect(mockOnAdd).toHaveBeenCalledWith('New todo');
});

it('clears input after adding todo', () => {
const mockOnAdd = vi.fn();
render(<AddTodo onAdd={mockOnAdd} />);
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);

const input = screen.getByPlaceholderText('Add a new todo...') as HTMLInputElement;
const button = screen.getByRole('button', { name: /add/i });

const button = screen.getByRole('button', { name: ADD_REGEX });
const form = button.closest('form') as HTMLFormElement;

fireEvent.change(input, { target: { value: 'New todo' } });
fireEvent.click(button);
fireEvent.submit(form);

expect(input.value).toBe('');
});

it('does not call onAdd with empty text', () => {
const mockOnAdd = vi.fn();
render(<AddTodo onAdd={mockOnAdd} />);

const button = screen.getByRole('button', { name: /add/i });
fireEvent.click(button);

renderWithRouter(<AddTodo onAdd={mockOnAdd} />);

const button = screen.getByRole('button', { name: ADD_REGEX });
const form = button.closest('form') as HTMLFormElement;
fireEvent.submit(form);

expect(mockOnAdd).not.toHaveBeenCalled();
});

it('trims whitespace from input', () => {
const mockOnAdd = vi.fn();
render(<AddTodo onAdd={mockOnAdd} />);
renderWithRouter(<AddTodo onAdd={mockOnAdd} />);

const input = screen.getByPlaceholderText('Add a new todo...');
const button = screen.getByRole('button', { name: /add/i });

const button = screen.getByRole('button', { name: ADD_REGEX });
const form = button.closest('form') as HTMLFormElement;

fireEvent.change(input, { target: { value: ' New todo ' } });
fireEvent.click(button);
fireEvent.submit(form);

expect(mockOnAdd).toHaveBeenCalledWith('New todo');
});
});

21 changes: 9 additions & 12 deletions apps/todo-app/app/components/add-todo.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { Plus } from 'lucide-react';
import { TextField, FormError } from '@lambdacurry/forms';
import { Button } from '@lambdacurry/forms/ui';

const addTodoSchema = z.object({
text: z.string().min(1, 'Todo text is required').trim(),
text: z.string().min(1, 'Todo text is required').trim()
});
Comment on lines +9 to 10
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix validation order: trim before min

As written, whitespace-only input passes .min(1) then gets trimmed to empty. Apply .trim() first, then .min(1).

-  text: z.string().min(1, 'Todo text is required').trim()
+  text: z.string().trim().min(1, 'Todo text is required')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text: z.string().min(1, 'Todo text is required').trim()
});
text: z.string().trim().min(1, 'Todo text is required')
});
🤖 Prompt for AI Agents
In apps/todo-app/app/components/add-todo.tsx around lines 9 to 10, the
validation chain applies .min(1) before .trim(), allowing whitespace-only input
to pass validation. To fix this, reorder the validation so that .trim() is
called first on the string, followed by .min(1) to ensure the trimmed input has
at least one character.


type AddTodoFormData = z.infer<typeof addTodoSchema>;
Expand All @@ -20,29 +20,26 @@ export function AddTodo({ onAdd }: AddTodoProps) {
resolver: zodResolver(addTodoSchema),
defaultValues: { text: '' },
submitHandlers: {
onValid: (data) => {
onValid: data => {
// Since we're not using a database we're catching this early and not actually submitting the form to a server
onAdd(data.text);
methods.reset();
},
},
}
}
});

return (
<RemixFormProvider {...methods}>
<form onSubmit={methods.handleSubmit} className="flex gap-2">
<form className="flex gap-2" onSubmit={methods.handleSubmit}>
<div className="flex-1">
<TextField
name="text"
placeholder="Add a new todo..."
className="w-full"
/>
<TextField name="text" placeholder="Add a new todo..." className="w-full" />
</div>
<Button type="submit">
<Plus className="h-4 w-4 mr-2" />
Add
</Button>
</form>
<FormError />
<FormError name="_form" />
</RemixFormProvider>
);
}
3 changes: 1 addition & 2 deletions apps/todo-app/app/components/todo-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function TodoFilters({
</Button>
))}
</div>

<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>{activeCount} active</span>
{completedCount > 0 && (
Expand All @@ -53,4 +53,3 @@ export function TodoFilters({
</div>
);
}

Loading