todo-next

yet another task list webapp, made with Next.js
Log | Files | Refs | README

commit ff25257f46cb45a8ea496adb2089774c306abb50
parent 41fd139f11e941af298cdaa11516818419292557
Author: massi <git@massi.world>
Date:   Fri, 11 Apr 2025 00:46:24 -0700

namespaces

Diffstat:
Asrc/app/todo-list/[title]/page.tsx | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/app/todo-list/page.tsx | 160+------------------------------------------------------------------------------
2 files changed, 205 insertions(+), 158 deletions(-)

diff --git a/src/app/todo-list/[title]/page.tsx b/src/app/todo-list/[title]/page.tsx @@ -0,0 +1,203 @@ +"use client"; +import { + ChangeEvent, + HTMLInputTypeAttribute, + InputHTMLAttributes, + KeyboardEventHandler, + ReactEventHandler, + useEffect, + useState, +} from "react"; + +import { useLocalStorage } from "@/lib/useLocalStorage"; +import { useParams, useRouter } from "next/navigation"; + +const MAX_TASK_LENGTH = 32; + +type TodoItem = { + description: string; + isDone: boolean; +}; + +function mkTodo(description: string): TodoItem { + return { + description, + isDone: false, + }; +} + +type CheckboxProps = { + isChecked: boolean; + onToggle: () => void; +}; + +function Checkbox(props: CheckboxProps) { + const { isChecked, onToggle } = props; + return ( + <div + className="cursor-pointer w-[28px] h-[28px] bg-gray-800 relative leading-none hover:bg-gray-700 select-none" + onClick={onToggle} + > + {isChecked ? "✅" : ""} + </div> + ); +} + +type TodoInputProps = { + onEnter: (desc: string) => void; +}; +function TodoInput(props: TodoInputProps) { + const [desc, setDesc] = useState<string>(""); + const { onEnter } = props; + const addItem = () => { + if (desc.length) { + onEnter(desc); + setDesc(() => ""); + } + }; + const handleKeyPress: KeyboardEventHandler<HTMLInputElement> = (evt) => { + switch (evt.key) { + case "Enter": { + evt.preventDefault(); + addItem(); + break; + } + } + }; + return ( + <div className="text-2xl p-2 mx-4"> + <input + value={desc} + onChange={(e) => setDesc(e.target.value)} + maxLength={MAX_TASK_LENGTH} + type="text" + onKeyDown={handleKeyPress} + placeholder="new todo goes here" + ></input> + <button onClick={addItem}>add</button> + </div> + ); +} + +type TodoItemProps = { + onDelete: () => void; + onToggleDone: () => void; + updateDescription: (description: string) => void; + todo: TodoItem; +}; + +function TodoItem(props: TodoItemProps) { + const { onDelete, onToggleDone, todo, updateDescription } = props; + return ( + <div className="group"> + <div className="text-2xl flex place-content-between p-2 mx-4 relative border-t-1 border-gray-600"> + <button + onClick={onDelete} + className="absolute left-[-10px] items-center top-0 cursor-pointer bottom-0 text-red-700 hidden group-hover:flex" + > + x + </button> + <input + value={todo.description} + onChange={(e) => updateDescription(e.target.value)} + maxLength={MAX_TASK_LENGTH} + ></input> + <Checkbox isChecked={todo.isDone} onToggle={onToggleDone} /> + </div> + </div> + ); +} + +type TitleProps = { title: string; rename: (newTitle: string) => boolean }; +function Title(props: TitleProps) { + const { title, rename } = props; + const [editingTitle, setEditingTitle] = useState<null | string>(null); + const handleKeyPress: KeyboardEventHandler<HTMLInputElement> = (evt) => { + switch (evt.key) { + case "Enter": { + evt.preventDefault(); + if (editingTitle) { + rename(editingTitle); + } + break; + } + } + }; + return ( + <h1 className="text-4xl"> + TODO:{" "} + <input + type="text" + value={editingTitle || title} + onChange={(evt) => setEditingTitle(evt.target.value)} + onKeyDown={handleKeyPress} + ></input> + </h1> + ); +} + +export default function TodoList() { + const router = useRouter(); + const { title } = useParams<{ title: string }>(); + const [items, setItems] = useLocalStorage<TodoItem[]>(title, []); + const renameTitle = (newTitle: string) => { + const dataAtNewTitle = !!localStorage.getItem(newTitle); + if (dataAtNewTitle) { + alert( + "AAAAAAAAAAAAAA THERES ALREADY A TODO LIST WITH THIS NAME AAAAAAAAAA CHANGE IT AAAAAAAAAAAAAAAAAAA", + ); + } else { + localStorage.removeItem(title); + localStorage.setItem(newTitle, JSON.stringify(items)); + router.replace(`/todo-list/${newTitle}`); + } + }; + return ( + <div className="flex flex-col items-center justify-items-center min-h-screen pb-20 gap-16 font-[family-name:var(--font-geist-sans)]"> + <header> + <Title title={title} rename={renameTitle} /> + </header> + <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start w-full"> + <div className="w-[100%]"> + <TodoInput + onEnter={(description: string) => + setItems((items) => [mkTodo(description), ...items]) + } + /> + {!items.length + ? "all done :)" + : items.map((todo, idx) => { + return ( + <TodoItem + todo={todo} + onToggleDone={() => + setItems((items) => { + const newItems = [...items]; + newItems[idx] = { + ...items[idx], + isDone: !items[idx].isDone, + }; + return newItems; + }) + } + onDelete={() => + setItems((items) => + items.filter((item, iidx) => idx !== iidx), + ) + } + updateDescription={(description: string) => + setItems((items) => + items.map((item, iidx) => + idx === iidx ? { ...item, description } : item, + ), + ) + } + key={idx} + /> + ); + })} + </div> + </main> + </div> + ); +} diff --git a/src/app/todo-list/page.tsx b/src/app/todo-list/page.tsx @@ -1,159 +1,3 @@ -"use client"; -import { - ChangeEvent, - HTMLInputTypeAttribute, - InputHTMLAttributes, - KeyboardEventHandler, - ReactEventHandler, - useState, -} from "react"; - -import { useLocalStorage } from "@/lib/useLocalStorage"; - -const MAX_TASK_LENGTH = 32; - -type TodoItem = { - description: string; - isDone: boolean; -}; - -function mkTodo(description: string): TodoItem { - return { - description, - isDone: false, - }; -} - -type CheckboxProps = { - isChecked: boolean; - onToggle: () => void; -}; - -function Checkbox(props: CheckboxProps) { - const { isChecked, onToggle } = props; - return ( - <div - className="cursor-pointer w-[28px] h-[28px] bg-gray-800 relative leading-none hover:bg-gray-700 select-none" - onClick={onToggle} - > - {isChecked ? "✅" : ""} - </div> - ); -} - -type TodoInputProps = { - onEnter: (desc: string) => void; -}; -function TodoInput(props: TodoInputProps) { - const [desc, setDesc] = useState<string>(""); - const { onEnter } = props; - const addItem = () => { - if (desc.length) { - onEnter(desc); - setDesc(() => ""); - } - }; - const handleKeyPress: KeyboardEventHandler<HTMLInputElement> = (evt) => { - switch (evt.key) { - case "Enter": { - evt.preventDefault(); - addItem(); - break; - } - } - }; - return ( - <div className="text-2xl p-2 mx-4"> - <input - value={desc} - onChange={(e) => setDesc(e.target.value)} - maxLength={MAX_TASK_LENGTH} - type="text" - onKeyDown={handleKeyPress} - placeholder="new todo goes here" - ></input> - <button onClick={addItem}>add</button> - </div> - ); -} - -type TodoItemProps = { - onDelete: () => void; - onToggleDone: () => void; - updateDescription: (description: string) => void; - todo: TodoItem; -}; - -function TodoItem(props: TodoItemProps) { - const { onDelete, onToggleDone, todo, updateDescription } = props; - return ( - <div className="group"> - <div className="text-2xl flex place-content-between p-2 mx-4 relative border-t-1 border-gray-600"> - <button - onClick={onDelete} - className="absolute left-[-10px] items-center top-0 cursor-pointer bottom-0 text-red-700 hidden group-hover:flex" - > - x - </button> - <input - value={todo.description} - onChange={(e) => updateDescription(e.target.value)} - maxLength={MAX_TASK_LENGTH} - ></input> - <Checkbox isChecked={todo.isDone} onToggle={onToggleDone} /> - </div> - </div> - ); -} - -export default function TodoList() { - const [items, setItems] = useLocalStorage<TodoItem[]>("todos", []); - return ( - <div className="flex flex-col items-center justify-items-center min-h-screen pb-20 gap-16 font-[family-name:var(--font-geist-sans)]"> - <header> - <h1 className="text-4xl">TODO APP</h1> - </header> - <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start w-full"> - <div className="w-[100%]"> - <TodoInput - onEnter={(description: string) => - setItems((items) => [mkTodo(description), ...items]) - } - /> - {!items.length - ? "all done :)" - : items.map((todo, idx) => { - return ( - <TodoItem - todo={todo} - onToggleDone={() => - setItems((items) => { - const newItems = [...items]; - newItems[idx] = { - ...items[idx], - isDone: !items[idx].isDone, - }; - return newItems; - }) - } - onDelete={() => - setItems((items) => - items.filter((item, iidx) => idx !== iidx), - ) - } - updateDescription={(description: string) => - setItems((items) => - items.map((item, iidx) => - idx === iidx ? { ...item, description } : item, - ), - ) - } - key={idx} - /> - ); - })} - </div> - </main> - </div> - ); +export default function Page() { + return <h2>howde?</h2>; }