Mercurial
comparison react_games/src/TODO/main.tsx @ 37:fb9bcd3145cb
[ReactGames] Few games I made using react just to practice few things.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 01 Dec 2025 20:22:47 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 36:84672efec192 | 37:fb9bcd3145cb |
|---|---|
| 1 /* | |
| 2 * 📝 React Todo App Challenge (1 Hour) | |
| 3 * ❓Problem | |
| 4 * Build a simple Todo application using React. The app should allow users to: | |
| 5 * | |
| 6 * - Add a new todo item. | |
| 7 * | |
| 8 * - Mark a todo as completed. | |
| 9 * | |
| 10 * - Delete a todo. | |
| 11 * | |
| 12 * Filter todos by: | |
| 13 * | |
| 14 * - All | |
| 15 * - Active | |
| 16 * - Completed | |
| 17 * | |
| 18 * 💡Bonus (if time allows) | |
| 19 * Persist todos in localStorage. | |
| 20 * | |
| 21 * Use React Context + useReducer to manage global state. | |
| 22 * | |
| 23 * Add a clear completed button. | |
| 24 * | |
| 25 * 📦 Requirements | |
| 26 * Use functional components + hooks only. | |
| 27 * | |
| 28 * | |
| 29 * | |
| 30 * No styling is required, but if you have time, make it readable. | |
| 31 * | |
| 32 * Don’t use external state management libraries like Redux. | |
| 33 * | |
| 34 * Components | |
| 35 * | |
| 36 * TODOs | |
| 37 * Inputs | |
| 38 * buttons filtering | |
| 39 * | |
| 40 * TODO States | |
| 41 * - Active | |
| 42 * - Completed | |
| 43 * - Deleted | |
| 44 */ | |
| 45 import React, { CSSProperties, Dispatch, InputHTMLAttributes, memo, useMemo, useRef, useState } from "react"; | |
| 46 | |
| 47 enum TodoState { | |
| 48 ACTIVE="active", | |
| 49 COMPLETED="completed", | |
| 50 DELETED="deleted", | |
| 51 } | |
| 52 | |
| 53 interface Todo { | |
| 54 id: string; | |
| 55 value: string; | |
| 56 state: TodoState; | |
| 57 } | |
| 58 | |
| 59 const INITIAL_TODOS: Todo[] = [ | |
| 60 {id: "1", value: "I want to buy pizza", state: TodoState.ACTIVE}, | |
| 61 {id: "2", value: "I want to buy pizza completed", state: TodoState.COMPLETED}, | |
| 62 {id: "3", value: "I want to buy pizza deleted", state: TodoState.DELETED}, | |
| 63 ] | |
| 64 | |
| 65 function changeStateTodo( | |
| 66 todoId: string, | |
| 67 todoState: TodoState, | |
| 68 setTodoCallback: Dispatch<React.SetStateAction<Todo[]>>, | |
| 69 ): void { | |
| 70 setTodoCallback( | |
| 71 (todos: Todo[]) => todos.map((todo: Todo) => { | |
| 72 if (todo.id === todoId) { | |
| 73 return { | |
| 74 ...todo, | |
| 75 state: todoState | |
| 76 }; | |
| 77 } | |
| 78 return todo; | |
| 79 }) | |
| 80 ) | |
| 81 } | |
| 82 | |
| 83 interface TodoComponentProp { | |
| 84 todo: Todo; | |
| 85 setTodos: Dispatch<React.SetStateAction<Todo[]>>; | |
| 86 } | |
| 87 | |
| 88 const TodoComponent = memo(({todo, setTodos}: TodoComponentProp) => { | |
| 89 const tdStyle: CSSProperties = { | |
| 90 padding: "0.5rem", | |
| 91 borderBottom: "1px solid #ddd", | |
| 92 }; | |
| 93 | |
| 94 const rowStyle: CSSProperties = { | |
| 95 backgroundColor: "#fff", | |
| 96 }; | |
| 97 | |
| 98 return ( | |
| 99 <tr key={todo.id} style={rowStyle}> | |
| 100 <td style={tdStyle}>{todo.value}</td> | |
| 101 <td style={tdStyle}> | |
| 102 <span style={{ textTransform: "capitalize" }}>{todo.state}</span> | |
| 103 </td> | |
| 104 <td style={tdStyle}> | |
| 105 {todo.state !== TodoState.COMPLETED && ( | |
| 106 <button onClick={() => changeStateTodo(todo.id, TodoState.COMPLETED, setTodos)}> | |
| 107 ✅ | |
| 108 </button> | |
| 109 )} | |
| 110 {todo.state !== TodoState.DELETED && ( | |
| 111 <button onClick={() => changeStateTodo(todo.id, TodoState.DELETED, setTodos)}> | |
| 112 🗑️ | |
| 113 </button> | |
| 114 )} | |
| 115 {todo.state !== TodoState.ACTIVE && ( | |
| 116 <button onClick={() => changeStateTodo(todo.id, TodoState.ACTIVE, setTodos)}> | |
| 117 🟢 | |
| 118 </button> | |
| 119 )} | |
| 120 </td> | |
| 121 </tr> | |
| 122 ) | |
| 123 }) | |
| 124 | |
| 125 const TODO = () => { | |
| 126 const [todos, setTodos] = useState<Todo[]>(INITIAL_TODOS); | |
| 127 const [todoFilter, setTodoFilters] = useState<TodoState | null>(null); | |
| 128 const inputRef = useRef<HTMLInputElement>(null); | |
| 129 | |
| 130 const style: CSSProperties = { | |
| 131 display: "flex", | |
| 132 justifyContent: "center", | |
| 133 alignItems: "center", | |
| 134 height: "100vh", | |
| 135 width: "100vw", | |
| 136 } | |
| 137 | |
| 138 const filterTodos = useMemo(() => { | |
| 139 return todoFilter ? todos.filter((t) => t.state === todoFilter) : todos; | |
| 140 }, [todos, todoFilter]); | |
| 141 | |
| 142 const handleCreateTodo = (event: React.FormEvent) => { | |
| 143 event.preventDefault(); | |
| 144 const input = inputRef.current; | |
| 145 if (!input || !input.value.trim()) return; | |
| 146 | |
| 147 const newValue = input.value.trim(); | |
| 148 setTodos( | |
| 149 (todos) => [ | |
| 150 ...todos, | |
| 151 { | |
| 152 value: newValue, | |
| 153 id: crypto.randomUUID(), | |
| 154 state: TodoState.ACTIVE | |
| 155 } | |
| 156 ] | |
| 157 ); | |
| 158 input.value = ""; | |
| 159 } | |
| 160 | |
| 161 const thStyle: CSSProperties = { | |
| 162 textAlign: "left", | |
| 163 padding: "0.5rem", | |
| 164 backgroundColor: "#f0f0f0", | |
| 165 borderBottom: "2px solid #ddd", | |
| 166 }; | |
| 167 | |
| 168 const buttonStyle = (isActive: boolean): CSSProperties => { | |
| 169 return { | |
| 170 padding: "0.5rem 1rem", | |
| 171 borderRadius: "6px", | |
| 172 border: "1px solid #ccc", | |
| 173 backgroundColor: isActive ? "#efefef" : "#fff", | |
| 174 marginBottom: "1rem", | |
| 175 } | |
| 176 }; | |
| 177 | |
| 178 return ( | |
| 179 <div style={style}> | |
| 180 <div> | |
| 181 <h1> TODOs: </h1> | |
| 182 <h2> Filter by: {todoFilter != null ? todoFilter : "All"} </h2> | |
| 183 <div style={{display: "flex", width: "100%", justifyContent: "space-between"}}> | |
| 184 <button style={buttonStyle(todoFilter===TodoState.ACTIVE)} onClick={() => setTodoFilters(TodoState.ACTIVE)}> Active </button> | |
| 185 <button style={buttonStyle(todoFilter===TodoState.DELETED)} onClick={() => setTodoFilters(TodoState.DELETED)}> Deleted </button> | |
| 186 <button style={buttonStyle(todoFilter===TodoState.COMPLETED)} onClick={() => setTodoFilters(TodoState.COMPLETED)}> Completed </button> | |
| 187 <button style={buttonStyle(todoFilter===null)} onClick={() => setTodoFilters(null)}> ALL </button> | |
| 188 </div> | |
| 189 <table style={{ borderCollapse: "collapse", width: "100%", marginBottom: "1rem" }}> | |
| 190 <thead> | |
| 191 <tr> | |
| 192 <th style={thStyle}>Task</th> | |
| 193 <th style={thStyle}>Status</th> | |
| 194 <th style={thStyle}>Action</th> | |
| 195 </tr> | |
| 196 </thead> | |
| 197 <tbody> | |
| 198 { | |
| 199 filterTodos.map((todo) => ( | |
| 200 <TodoComponent key={todo.id} todo={todo} setTodos={setTodos}/> | |
| 201 )) | |
| 202 } | |
| 203 </tbody> | |
| 204 </table> | |
| 205 <form onSubmit={handleCreateTodo}> | |
| 206 <input ref={inputRef} placeholder={"Add a new todo..."} style={{ | |
| 207 padding: "0.5rem", | |
| 208 border: "1px solid #ccc", | |
| 209 borderRadius: "4px", | |
| 210 width: "300px", | |
| 211 marginRight: "0.5rem", | |
| 212 }} | |
| 213 /> | |
| 214 <button type="submit" style={{ padding: "0.5rem 1rem", borderRadius: "4px" }}>Add</button> | |
| 215 </form> | |
| 216 </div> | |
| 217 </div> | |
| 218 ); | |
| 219 } | |
| 220 | |
| 221 export { | |
| 222 TODO | |
| 223 } |