diff --git a/plan.txt b/plan.txt
new file mode 100644
index 0000000..f52b622
--- /dev/null
+++ b/plan.txt
@@ -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
\ No newline at end of file
diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx
index 2344883..45fe7a6 100644
--- a/src/ChallengeComponent.tsx
+++ b/src/ChallengeComponent.tsx
@@ -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. */}
-
- Your code goes here
-
- >
+
+
+ {categories.map(category => {
+ const categoryTodos = getCategoryItems(category.id);
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
);
}
diff --git a/src/components/Category.tsx b/src/components/Category.tsx
new file mode 100644
index 0000000..47633ca
--- /dev/null
+++ b/src/components/Category.tsx
@@ -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 (
+
+
+
{category.name}
+
+
+ {todos.map(todo => (
+
+ ))}
+
+ )
+}
+
+export default CategoryColumn;
\ No newline at end of file
diff --git a/src/components/NewTodoInput.tsx b/src/components/NewTodoInput.tsx
new file mode 100644
index 0000000..be9d023
--- /dev/null
+++ b/src/components/NewTodoInput.tsx
@@ -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) => {
+ setTodoName(e.target.value);
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ const trimmedTodoName = todoName.trim();
+ if (!trimmedTodoName) {
+ return;
+ }
+
+ e.preventDefault();
+ onSubmit(todoName);
+ setTodoName("");
+ };
+
+ return (
+
+ )
+};
+
+export default NewTodoInput;
\ No newline at end of file
diff --git a/src/components/ToDoItem.tsx b/src/components/ToDoItem.tsx
new file mode 100644
index 0000000..5605ab2
--- /dev/null
+++ b/src/components/ToDoItem.tsx
@@ -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 (
+
+ )
+}
+
+export default ToDoItem;
\ No newline at end of file
diff --git a/src/components/TodoButton.tsx b/src/components/TodoButton.tsx
new file mode 100644
index 0000000..cd0b0af
--- /dev/null
+++ b/src/components/TodoButton.tsx
@@ -0,0 +1,18 @@
+interface TodoButtonProps {
+ direction: 'left' | 'right';
+ disabled: boolean;
+ handleClick: (direction: 'left' | 'right') => void;
+}
+const TodoButton = ({ direction, disabled, handleClick }: TodoButtonProps) => {
+ return (
+ handleClick(direction)}
+ >
+ {direction === 'left' ? '◀' : '▶'}
+
+ );
+};
+
+export default TodoButton;
\ No newline at end of file
diff --git a/src/constants/index.ts b/src/constants/index.ts
new file mode 100644
index 0000000..d129636
--- /dev/null
+++ b/src/constants/index.ts
@@ -0,0 +1,5 @@
+export const CATEGORY_NAMES = [
+ "To Do",
+ "In Progress",
+ "Done",
+]
\ No newline at end of file
diff --git a/src/hooks/useTodos.test.ts b/src/hooks/useTodos.test.ts
new file mode 100644
index 0000000..8a4dcf8
--- /dev/null
+++ b/src/hooks/useTodos.test.ts
@@ -0,0 +1,446 @@
+import { act } from "react";
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { renderHook } from "@testing-library/react";
+import useTodos from "./useTodos";
+import * as utils from "../utils";
+
+// Mock the utils module to control UUID generation
+vi.mock("../utils", async () => {
+ const actual = await vi.importActual("../utils");
+ return {
+ ...actual,
+ newUUID: vi.fn(),
+ };
+});
+
+describe("useTodos", () => {
+ let mockUUIDCounter = 0;
+
+ beforeEach(() => {
+ mockUUIDCounter = 0;
+ // Reset and setup mock UUID generator
+ vi.mocked(utils.newUUID).mockImplementation(() => {
+ return `mock-uuid-${mockUUIDCounter++}`;
+ });
+ });
+
+ describe("initialization", () => {
+ it("should initialize categories from provided category names", () => {
+ const categoryNames = ["Work", "Personal", "Shopping"];
+ const { result } = renderHook(() => useTodos(categoryNames));
+
+ expect(result.current.categories).toHaveLength(3);
+ expect(result.current.categories[0].name).toBe("Work");
+ expect(result.current.categories[1].name).toBe("Personal");
+ expect(result.current.categories[2].name).toBe("Shopping");
+ });
+
+ it("should generate unique IDs for categories", () => {
+ const { result } = renderHook(() => useTodos(["Work", "Personal"]));
+
+ const categoryIds = result.current.categories.map((cat) => cat.id);
+ const uniqueIds = new Set(categoryIds);
+ expect(uniqueIds.size).toBe(categoryIds.length);
+ });
+
+ it("should handle empty category names array", () => {
+ const { result } = renderHook(() => useTodos([]));
+
+ expect(result.current.categories).toEqual([]);
+ expect(result.current.todos).toEqual([]);
+ });
+ });
+
+ describe("createTodo", () => {
+ it("should create a new todo with the given name", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ act(() => {
+ result.current.createTodo("Buy groceries");
+ });
+
+ expect(result.current.todos).toHaveLength(1);
+ expect(result.current.todos[0].name).toBe("Buy groceries");
+ });
+
+ it("should assign new todo to the first category", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const firstCategoryId = result.current.categories[0].id;
+
+ act(() => {
+ result.current.createTodo("Complete task");
+ });
+
+ expect(result.current.todos[0].categoryId).toBe(firstCategoryId);
+ });
+
+ it("should generate unique IDs for todos", () => {
+ const { result } = renderHook(() => useTodos(["Work"]));
+
+ act(() => {
+ result.current.createTodo("Task 1");
+ result.current.createTodo("Task 2");
+ result.current.createTodo("Task 3");
+ });
+
+ const todoIds = result.current.todos.map((todo) => todo.id);
+ const uniqueIds = new Set(todoIds);
+ expect(uniqueIds.size).toBe(todoIds.length);
+ });
+
+ it("should create multiple todos", () => {
+ const { result } = renderHook(() => useTodos(["Work"]));
+
+ act(() => {
+ result.current.createTodo("Task 1");
+ });
+
+ act(() => {
+ result.current.createTodo("Task 2");
+ });
+
+ act(() => {
+ result.current.createTodo("Task 3");
+ });
+
+ expect(result.current.todos).toHaveLength(3);
+ expect(result.current.todos[0].name).toBe("Task 1");
+ expect(result.current.todos[1].name).toBe("Task 2");
+ expect(result.current.todos[2].name).toBe("Task 3");
+ });
+
+ it("should throw error when no categories exist", () => {
+ const { result } = renderHook(() => useTodos([]));
+
+ expect(() => {
+ act(() => {
+ result.current.createTodo("Task without category");
+ });
+ }).toThrow("No initial category found");
+ });
+
+ it("should handle empty todo name", () => {
+ const { result } = renderHook(() => useTodos(["Work"]));
+
+ act(() => {
+ result.current.createTodo("");
+ });
+
+ expect(result.current.todos).toHaveLength(1);
+ expect(result.current.todos[0].name).toBe("");
+ });
+
+ it("should handle todo names with special characters", () => {
+ const { result } = renderHook(() => useTodos(["Work"]));
+
+ const specialName = "Task with @#$%^&*() characters!";
+
+ act(() => {
+ result.current.createTodo(specialName);
+ });
+
+ expect(result.current.todos[0].name).toBe(specialName);
+ });
+ });
+
+ describe("getCategoryItems", () => {
+ it("should return empty array for category with no todos", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const categoryId = result.current.categories[0].id;
+ const items = result.current.getCategoryItems(categoryId);
+
+ expect(items).toEqual([]);
+ });
+
+ it("should return todos for a specific category", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const firstCategoryId = result.current.categories[0].id;
+
+ act(() => {
+ result.current.createTodo("Task 1");
+ });
+
+ act(() => {
+ result.current.createTodo("Task 2");
+ });
+
+ const items = result.current.getCategoryItems(firstCategoryId);
+
+ expect(items).toHaveLength(2);
+ expect(items[0].name).toBe("Task 1");
+ expect(items[1].name).toBe("Task 2");
+ });
+
+ it("should return empty array for non-existent category", () => {
+ const { result } = renderHook(() => useTodos(["Work"]));
+
+ act(() => {
+ result.current.createTodo("Task 1");
+ });
+
+ const items = result.current.getCategoryItems("non-existent-id");
+
+ expect(items).toEqual([]);
+ });
+
+ it("should return empty array for undefined category", () => {
+ const { result } = renderHook(() => useTodos(["Work"]));
+
+ const items = result.current.getCategoryItems("undefined-category");
+
+ expect(items).toEqual([]);
+ });
+
+ it("should correctly separate todos by category", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const workCategoryId = result.current.categories[0].id;
+ const personalCategoryId = result.current.categories[1].id;
+ const shoppingCategoryId = result.current.categories[2].id;
+
+ // Manually create todos in different categories
+ act(() => {
+ result.current.createTodo("Work task 1");
+ });
+
+ act(() => {
+ result.current.createTodo("Work task 2");
+ });
+
+ // At this point all todos are in the first category (Work)
+ const workItems = result.current.getCategoryItems(workCategoryId);
+ const personalItems = result.current.getCategoryItems(personalCategoryId);
+ const shoppingItems = result.current.getCategoryItems(shoppingCategoryId);
+
+ expect(workItems).toHaveLength(2);
+ expect(personalItems).toHaveLength(0);
+ expect(shoppingItems).toHaveLength(0);
+ });
+ });
+
+ describe("isCategoryAtEdge", () => {
+ it("should return true for first category when direction is left", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const firstCategoryId = result.current.categories[0].id;
+ const isAtEdge = result.current.isCategoryAtEdge(firstCategoryId, "left");
+
+ expect(isAtEdge).toBe(true);
+ });
+
+ it("should return false for first category when direction is right", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const firstCategoryId = result.current.categories[0].id;
+ const isAtEdge = result.current.isCategoryAtEdge(firstCategoryId, "right");
+
+ expect(isAtEdge).toBe(false);
+ });
+
+ it("should return true for last category when direction is right", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const lastCategoryId = result.current.categories[2].id;
+ const isAtEdge = result.current.isCategoryAtEdge(lastCategoryId, "right");
+
+ expect(isAtEdge).toBe(true);
+ });
+
+ it("should return false for last category when direction is left", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const lastCategoryId = result.current.categories[2].id;
+ const isAtEdge = result.current.isCategoryAtEdge(lastCategoryId, "left");
+
+ expect(isAtEdge).toBe(false);
+ });
+
+ it("should return false for middle category in both directions", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const middleCategoryId = result.current.categories[1].id;
+ const isAtLeftEdge = result.current.isCategoryAtEdge(middleCategoryId, "left");
+ const isAtRightEdge = result.current.isCategoryAtEdge(middleCategoryId, "right");
+
+ expect(isAtLeftEdge).toBe(false);
+ expect(isAtRightEdge).toBe(false);
+ });
+
+ it("should return true for single category in both directions", () => {
+ const { result } = renderHook(() => useTodos(["OnlyCategory"]));
+
+ const categoryId = result.current.categories[0].id;
+ const isAtLeftEdge = result.current.isCategoryAtEdge(categoryId, "left");
+ const isAtRightEdge = result.current.isCategoryAtEdge(categoryId, "right");
+
+ expect(isAtLeftEdge).toBe(true);
+ expect(isAtRightEdge).toBe(true);
+ });
+
+ it("should work correctly with two categories", () => {
+ const { result } = renderHook(() => useTodos(["First", "Second"]));
+
+ const firstId = result.current.categories[0].id;
+ const secondId = result.current.categories[1].id;
+
+ expect(result.current.isCategoryAtEdge(firstId, "left")).toBe(true);
+ expect(result.current.isCategoryAtEdge(firstId, "right")).toBe(false);
+ expect(result.current.isCategoryAtEdge(secondId, "left")).toBe(false);
+ expect(result.current.isCategoryAtEdge(secondId, "right")).toBe(true);
+ });
+ });
+
+ describe("moveTodo", () => {
+ it("should move todo from first to second category when direction is right", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const firstCategoryId = result.current.categories[0].id;
+ const secondCategoryId = result.current.categories[1].id;
+
+ act(() => {
+ result.current.createTodo("Test Task");
+ });
+
+ const todoId = result.current.todos[0].id;
+ expect(result.current.todos[0].categoryId).toBe(firstCategoryId);
+
+ act(() => {
+ result.current.moveTodo(todoId, "right");
+ });
+
+ expect(result.current.todos[0].categoryId).toBe(secondCategoryId);
+ });
+
+ it("should move todo from second to first category when direction is left", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ const firstCategoryId = result.current.categories[0].id;
+ const secondCategoryId = result.current.categories[1].id;
+
+ act(() => {
+ result.current.createTodo("Test Task");
+ });
+
+ const todoId = result.current.todos[0].id;
+
+ // Move right first
+ act(() => {
+ result.current.moveTodo(todoId, "right");
+ });
+
+ expect(result.current.todos[0].categoryId).toBe(secondCategoryId);
+
+ // Move back left
+ act(() => {
+ result.current.moveTodo(todoId, "left");
+ });
+
+ expect(result.current.todos[0].categoryId).toBe(firstCategoryId);
+ });
+
+ it("should throw an error when moving todo left from first category", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ act(() => {
+ result.current.createTodo("Test Task");
+ });
+
+ const todoId = result.current.todos[0].id;
+
+ expect(() => {
+ act(() => {
+ result.current.moveTodo(todoId, "left");
+ });
+ }).toThrow("New category not found");
+ });
+
+ it("should throw an error when moving todo right from last category", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ act(() => {
+ result.current.createTodo("Test Task");
+ });
+
+ const todoId = result.current.todos[0].id;
+
+ // Move to last category
+ act(() => {
+ result.current.moveTodo(todoId, "right");
+ });
+
+ act(() => {
+ result.current.moveTodo(todoId, "right");
+ });
+
+ expect(() => {
+ act(() => {
+ result.current.moveTodo(todoId, "right");
+ });
+ }).toThrow("New category not found");
+ });
+
+ it("should throw error when todo is not found", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ expect(() => {
+ act(() => {
+ result.current.moveTodo("non-existent-id", "right");
+ });
+ }).toThrow("Todo not found");
+ });
+
+ it("should preserve todo properties when moving", () => {
+ const { result } = renderHook(() =>
+ useTodos(["Work", "Personal", "Shopping"])
+ );
+
+ act(() => {
+ result.current.createTodo("Important Task");
+ });
+
+ const todoId = result.current.todos[0].id;
+ const todoName = result.current.todos[0].name;
+
+ act(() => {
+ result.current.moveTodo(todoId, "right");
+ });
+
+ expect(result.current.todos[0].id).toBe(todoId);
+ expect(result.current.todos[0].name).toBe(todoName);
+ });
+ });
+});
+
diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts
new file mode 100644
index 0000000..0e9b978
--- /dev/null
+++ b/src/hooks/useTodos.ts
@@ -0,0 +1,114 @@
+import { useEffect, useState } from "react";
+import { createCategories, createTodo as createTodoUtil } from "../utils"
+import { Category, Todo } from "../types";
+
+interface UseTodosAPI {
+ categories: Category[];
+ todos: Todo[];
+ createTodo: (name: string) => void;
+ getCategoryItems: (categoryId: string) => Todo[];
+ isCategoryAtEdge: (categoryId: string, direction: 'left' | 'right') => boolean;
+ moveTodo: (targetTodoId: string, direction: 'left' | 'right') => void;
+}
+
+interface CategoryTodoMap {
+ [categoryId: Category["id"]]: Todo[];
+}
+
+const useTodos = (categoryNames: string[]): UseTodosAPI => {
+ const [categories, _setCategories] = useState(createCategories(categoryNames));
+ const [todos, setTodos] = useState([]);
+ const [categoryTodoMap, setCategoryTodoMap] = useState({});
+
+ const getInitialCategory = () => {
+ return categories[0];
+ };
+
+ const createTodo = (name: string) => {
+ const initialCategory = getInitialCategory();
+ if (!initialCategory) {
+ throw new Error("No initial category found");
+ }
+
+ const newTodo = createTodoUtil(name, initialCategory.id);
+ setTodos([...todos, newTodo]);
+ };
+
+ const getCategoryItems = (categoryId: string) => {
+ return categoryTodoMap[categoryId] || [];
+ };
+
+ const isCategoryAtEdge = (categoryId: string, direction: 'left' | 'right') => {
+ if (direction === 'left') {
+ return categoryId === categories[0].id;
+ } else {
+ return categoryId === categories[categories.length - 1].id;
+ }
+ };
+
+ const moveTodo = (targetTodoId: string, direction: 'left' | 'right') => {
+
+ const todo = todos.find(todo => todo.id === targetTodoId);
+ if (!todo) {
+ throw new Error("Todo not found");
+ }
+
+ const currentCategory = categories.find(category => category.id === todo.categoryId);
+ if (!currentCategory) {
+ throw new Error("Current category not found");
+ }
+
+ const currentCategoryIndex = categories.indexOf(currentCategory);
+ if (currentCategoryIndex === -1) {
+ throw new Error("Current category not found");
+ }
+
+ const newCategoryIndex = currentCategoryIndex + (direction === 'left' ? -1 : 1);
+ if (newCategoryIndex < 0 || newCategoryIndex >= categories.length) {
+ throw new Error("New category not found");
+ }
+
+ const newCategory = categories[newCategoryIndex];
+ if (!newCategory) {
+ throw new Error("New category not found");
+ }
+
+ setTodos(todos.map(todo => {
+ if (todo.id === targetTodoId) {
+ return {
+ ...todo,
+ categoryId: newCategory.id,
+ };
+ }
+
+ return todo;
+ }));
+ };
+
+ /*
+ In reality, implementing something like this is overkill for merely
+ needing to get all the todos for a given category, but figured it'd demonstrate
+ an awareness of how to structure data to reduce superfluous computation.
+ You could also just stick todos inside categories (in memory), but that's more
+ cumbersome imho.
+ */
+ useEffect(() => {
+ const newCategoryTodoMap = todos.reduce((acc, todo) => {
+ acc[todo.categoryId] = [...(acc[todo.categoryId] || []), todo];
+ return acc;
+ }, {});
+
+ setCategoryTodoMap(newCategoryTodoMap);
+ }, [todos, categories]);
+
+ return {
+ categories,
+ todos,
+ createTodo,
+ getCategoryItems,
+ isCategoryAtEdge,
+ moveTodo,
+ };
+}
+
+export default useTodos;
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..c01d28a
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,10 @@
+export type Todo = {
+ id: string;
+ name: string;
+ categoryId: string;
+}
+
+export type Category = {
+ id: string;
+ name: string;
+}
\ No newline at end of file
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..fb3a0af
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,20 @@
+import { Category, Todo } from "../types";
+
+export const newUUID = (): string => {
+ return crypto.randomUUID();
+}
+
+export const createCategories = (categoryNames: string[]): Category[] => {
+ return categoryNames.map(name => ({
+ id: newUUID(),
+ name,
+ }));
+}
+
+export const createTodo = (name: string, categoryId: string): Todo => {
+ return {
+ id: newUUID(),
+ name,
+ categoryId,
+ }
+}
\ No newline at end of file