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
40 changes: 40 additions & 0 deletions plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
A scratch pad for me to think through my plan of attack. normally i just scribble
this kind of stuff onto whatever scrap paper is closest to me, but figured
the extra insight for y'all wouldnt hurt

El plan:

- `useTodos` hook?
- accept arbitrary number/order of categories
- use array order for now, keep it simple
- generate a UUID in memory for each category, just to avoid key conflicts that could result from reusing category names

- return API for creating and moving todos
- createTodo(name: string) -> Todo
- canMoveTodo(todoId: string, direction: 'left' | 'right') -> boolean
- this should probs live at the 'category' level, actually
- when rendering each category, have it pass this bool into each todo

- moveTodo(todoId: string, direction: 'left' | 'right') -> void

- tests
- i assume claude will be able to one shot proper test coverage for this but i guess we'll see
- spoiler: it did not
- unit tests for new hook API, a couple integration tests for testing the whole jawn
- I ended up skipping the integration tests at the end, just for time's sake. there isn't
much that's actually being "integrated" anyway, so figured this was fine considering the
unit test coverage for the hook
- manually smoke test everything

- components
- todoList (wrapper)
- category
- todoItem
- moveTodoButton
- newTodoInput

- random
- local storage persistence??
- nah
- shortcuts/accessibility/tab focus/etc???
- also nah
35 changes: 29 additions & 6 deletions src/ChallengeComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import CategoryColumn from "./components/Category";
import NewTodoInput from "./components/NewTodoInput";
import { CATEGORY_NAMES } from "./constants";
import useTodos from "./hooks/useTodos";

export function ChallengeComponent() {
const { categories, createTodo, getCategoryItems, isCategoryAtEdge, moveTodo } = useTodos(CATEGORY_NAMES);

return (
<>
{/* Delete this h2, and add your own code here. */}
<h2 className="text-center py-48 text-xl text-gray-700">
Your code goes here
</h2>
</>
<div className="p-6">
<div className="flex flex-row justify-between gap-4">
{categories.map(category => {
const categoryTodos = getCategoryItems(category.id);

return (
<CategoryColumn
key={category.id}
category={category}
todos={categoryTodos}
moveTodo={moveTodo}
isLeftmostCategory={isCategoryAtEdge(category.id, 'left')}
isRightmostCategory={isCategoryAtEdge(category.id, 'right')}
/>
);
})}
</div>

<div className="mt-4">
<NewTodoInput onSubmit={createTodo} />
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions src/components/Category.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Category, Todo } from "../types";
import ToDoItem from "./ToDoItem";

interface CategoryColumnProps {
category: Category;
todos: Todo[];
isLeftmostCategory?: boolean;
isRightmostCategory?: boolean;
moveTodo: (targetTodoId: string, direction: 'left' | 'right') => void;
}

const CategoryColumn = ({ category, todos, isLeftmostCategory = false, isRightmostCategory = false, moveTodo }: CategoryColumnProps) => {
return (
<div className="flex-1 border border-gray-200 p-4 rounded-lg shadow-md min-h-96">
<div className="text-center">
<h2 className="text-2xl">{category.name}</h2>
</div>

{todos.map(todo => (
<ToDoItem key={todo.id} todo={todo} canMoveLeft={!isLeftmostCategory} canMoveRight={!isRightmostCategory} moveTodo={moveTodo} />
))}
</div>
)
}

export default CategoryColumn;
47 changes: 47 additions & 0 deletions src/components/NewTodoInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from "react";

interface NewTodoInputProps {
onSubmit: (todoName: string) => void;
}

const NewTodoInput = ({ onSubmit }: NewTodoInputProps) => {
const [todoName, setTodoName] = useState("");

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTodoName(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const trimmedTodoName = todoName.trim();
if (!trimmedTodoName) {
return;
}

e.preventDefault();
onSubmit(todoName);
setTodoName("");
};

return (
<form onSubmit={handleSubmit}>
<input
className="border border-gray-200 p-2 rounded-lg shadow-md"
name="todoName"
placeholder="Add Task"
type="text"
onChange={handleChange}
value={todoName}
/>

<button
className="bg-blue-500 text-white p-2 rounded-lg shadow-md hover:bg-blue-600 transition-colors px-4 ml-2"
type="submit"
disabled={!todoName.trim()}
>
+
</button>
</form>
)
};

export default NewTodoInput;
25 changes: 25 additions & 0 deletions src/components/ToDoItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Todo } from "../types";
import TodoButton from "./TodoButton";

interface ToDoItemProps {
todo: Todo;
canMoveLeft: boolean;
canMoveRight: boolean;
moveTodo: (targetTodoId: string, direction: 'left' | 'right') => void;
}

const ToDoItem = ({ todo, canMoveLeft, canMoveRight, moveTodo }: ToDoItemProps) => {
const handleMoveTodo = (direction: 'left' | 'right') => {
moveTodo(todo.id, direction);
};

return (
<div className="flex flex-row justify-between items-center border border-gray-200 p-2 rounded-lg shadow-md">
<TodoButton direction="left" disabled={!canMoveLeft} handleClick={handleMoveTodo} />
<div>{todo.name}</div>
<TodoButton direction="right" disabled={!canMoveRight} handleClick={handleMoveTodo} />
</div>
)
}

export default ToDoItem;
18 changes: 18 additions & 0 deletions src/components/TodoButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
interface TodoButtonProps {
direction: 'left' | 'right';
disabled: boolean;
handleClick: (direction: 'left' | 'right') => void;
}
const TodoButton = ({ direction, disabled, handleClick }: TodoButtonProps) => {
return (
<button
disabled={disabled}
className={`w-10 h-10 rounded-full ${direction === 'left' ? 'bg-red-500' : 'bg-blue-500'} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={() => handleClick(direction)}
>
{direction === 'left' ? '◀' : '▶'}
</button>
);
};

export default TodoButton;
5 changes: 5 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const CATEGORY_NAMES = [
"To Do",
"In Progress",
"Done",
]
Loading