Created
October 2, 2025 17:15
-
-
Save dimasusername/e98d55cd1cc5c25f3fd78650fc76d280 to your computer and use it in GitHub Desktop.
GPT-5 One-Shot ToDo with `create to-do app in react with vite`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useEffect, useMemo, useRef, useState } from "react"; | |
| // Single-file React To‑Do app | |
| // Features: add, edit, complete, delete, filter (All/Active/Completed), | |
| // clear completed, reorder via drag, and localStorage persistence. | |
| // Works out of the box in a Vite React template by replacing App.jsx. | |
| const STORAGE_KEY = "todoapp.v1.items"; | |
| function uid() { | |
| if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); | |
| return Math.random().toString(36).slice(2) + Date.now().toString(36); | |
| } | |
| export default function App() { | |
| const [items, setItems] = useState(() => { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| return raw ? JSON.parse(raw) : []; | |
| } catch { | |
| return []; | |
| } | |
| }); | |
| const [filter, setFilter] = useState("all"); // all | active | completed | |
| const [draft, setDraft] = useState(""); | |
| const [editingId, setEditingId] = useState(null); | |
| const [editingText, setEditingText] = useState(""); | |
| const inputRef = useRef(null); | |
| useEffect(() => { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); | |
| }, [items]); | |
| const left = items.filter((t) => !t.done).length; | |
| const filtered = useMemo(() => { | |
| switch (filter) { | |
| case "active": | |
| return items.filter((t) => !t.done); | |
| case "completed": | |
| return items.filter((t) => t.done); | |
| default: | |
| return items; | |
| } | |
| }, [items, filter]); | |
| function addItem() { | |
| const text = draft.trim(); | |
| if (!text) return; | |
| setItems((prev) => [{ id: uid(), text, done: false }, ...prev]); | |
| setDraft(""); | |
| inputRef.current?.focus(); | |
| } | |
| function toggle(id) { | |
| setItems((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))); | |
| } | |
| function remove(id) { | |
| setItems((prev) => prev.filter((t) => t.id !== id)); | |
| } | |
| function beginEdit(it) { | |
| setEditingId(it.id); | |
| setEditingText(it.text); | |
| } | |
| function commitEdit() { | |
| if (!editingId) return; | |
| const text = editingText.trim(); | |
| if (!text) { | |
| // Empty means delete | |
| setItems((prev) => prev.filter((t) => t.id !== editingId)); | |
| } else { | |
| setItems((prev) => prev.map((t) => (t.id === editingId ? { ...t, text } : t))); | |
| } | |
| setEditingId(null); | |
| setEditingText(""); | |
| } | |
| function clearCompleted() { | |
| setItems((prev) => prev.filter((t) => !t.done)); | |
| } | |
| // Simple drag to reorder | |
| const dragSrcId = useRef(null); | |
| function onDragStart(e, id) { | |
| dragSrcId.current = id; | |
| e.dataTransfer.effectAllowed = "move"; | |
| } | |
| function onDragOver(e) { | |
| e.preventDefault(); | |
| e.dataTransfer.dropEffect = "move"; | |
| } | |
| function onDrop(e, targetId) { | |
| e.preventDefault(); | |
| const srcId = dragSrcId.current; | |
| if (!srcId || srcId === targetId) return; | |
| setItems((prev) => { | |
| const srcIdx = prev.findIndex((t) => t.id === srcId); | |
| const tgtIdx = prev.findIndex((t) => t.id === targetId); | |
| if (srcIdx === -1 || tgtIdx === -1) return prev; | |
| const next = prev.slice(); | |
| const [moved] = next.splice(srcIdx, 1); | |
| next.splice(tgtIdx, 0, moved); | |
| return next; | |
| }); | |
| dragSrcId.current = null; | |
| } | |
| return ( | |
| <div style={styles.page}> | |
| <div style={styles.card}> | |
| <header style={styles.header}> | |
| <h1 style={{ margin: 0 }}>✅ To‑Do</h1> | |
| <p style={{ margin: 0, opacity: 0.7 }}>React + Vite • Local first</p> | |
| </header> | |
| <div style={styles.inputRow}> | |
| <input | |
| ref={inputRef} | |
| style={styles.input} | |
| placeholder="Add a task and press Enter" | |
| value={draft} | |
| onChange={(e) => setDraft(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter") addItem(); | |
| }} | |
| /> | |
| <button style={styles.primaryBtn} onClick={addItem} aria-label="Add task"> | |
| Add | |
| </button> | |
| </div> | |
| <div style={styles.toolbar}> | |
| <div role="tablist" aria-label="Filter tasks" style={{ display: "flex", gap: 8 }}> | |
| {[ | |
| { id: "all", label: "All" }, | |
| { id: "active", label: "Active" }, | |
| { id: "completed", label: "Completed" }, | |
| ].map(({ id, label }) => ( | |
| <button | |
| key={id} | |
| role="tab" | |
| aria-selected={filter === id} | |
| onClick={() => setFilter(id)} | |
| style={{ | |
| ...styles.chip, | |
| ...(filter === id ? styles.chipActive : null), | |
| }} | |
| > | |
| {label} | |
| </button> | |
| ))} | |
| </div> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8 }}> | |
| <span style={{ opacity: 0.7 }}>{left} left</span> | |
| <button style={styles.ghostBtn} onClick={clearCompleted}> | |
| Clear completed | |
| </button> | |
| </div> | |
| </div> | |
| <ul style={styles.list}> | |
| {filtered.map((it) => ( | |
| <li | |
| key={it.id} | |
| style={styles.item} | |
| draggable | |
| onDragStart={(e) => onDragStart(e, it.id)} | |
| onDragOver={onDragOver} | |
| onDrop={(e) => onDrop(e, it.id)} | |
| > | |
| <label style={styles.checkboxLabel}> | |
| <input | |
| type="checkbox" | |
| checked={!!it.done} | |
| onChange={() => toggle(it.id)} | |
| aria-label={it.done ? "Mark as active" : "Mark as completed"} | |
| /> | |
| </label> | |
| {editingId === it.id ? ( | |
| <input | |
| autoFocus | |
| style={{ ...styles.text, ...styles.editInput }} | |
| value={editingText} | |
| onChange={(e) => setEditingText(e.target.value)} | |
| onBlur={commitEdit} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter") commitEdit(); | |
| if (e.key === "Escape") { | |
| setEditingId(null); | |
| setEditingText(""); | |
| } | |
| }} | |
| /> | |
| ) : ( | |
| <span | |
| onDoubleClick={() => beginEdit(it)} | |
| style={{ | |
| ...styles.text, | |
| textDecoration: it.done ? "line-through" : "none", | |
| opacity: it.done ? 0.6 : 1, | |
| cursor: "text", | |
| }} | |
| title="Double‑click to edit" | |
| > | |
| {it.text} | |
| </span> | |
| )} | |
| <div style={styles.itemBtns}> | |
| <button style={styles.iconBtn} onClick={() => beginEdit(it)} title="Edit"> | |
| ✏️ | |
| </button> | |
| <button style={styles.iconBtn} onClick={() => remove(it.id)} title="Delete"> | |
| 🗑️ | |
| </button> | |
| </div> | |
| </li> | |
| ))} | |
| {filtered.length === 0 && ( | |
| <li style={{ padding: 16, textAlign: "center", color: "#667" }}> | |
| No tasks here. Enjoy the calm ✨ | |
| </li> | |
| )} | |
| </ul> | |
| </div> | |
| <footer style={styles.footer}> | |
| <a href="https://vitejs.dev/" target="_blank" rel="noreferrer"> | |
| Vite | |
| </a> | |
| <span> • </span> | |
| <a href="https://react.dev/" target="_blank" rel="noreferrer"> | |
| React | |
| </a> | |
| </footer> | |
| </div> | |
| ); | |
| } | |
| const styles = { | |
| page: { | |
| minHeight: "100dvh", | |
| background: "linear-gradient(180deg,#0f172a 0%, #0b1024 100%)", | |
| color: "#e5e7eb", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| padding: 24, | |
| boxSizing: "border-box", | |
| fontFamily: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, \"Apple Color Emoji\", \"Segoe UI Emoji\"", | |
| }, | |
| card: { | |
| width: "min(720px, 100%)", | |
| background: "#0b122c", | |
| border: "1px solid rgba(255,255,255,0.08)", | |
| borderRadius: 16, | |
| boxShadow: "0 10px 30px rgba(0,0,0,0.35)", | |
| padding: 20, | |
| }, | |
| header: { | |
| display: "flex", | |
| alignItems: "baseline", | |
| justifyContent: "space-between", | |
| marginBottom: 12, | |
| }, | |
| inputRow: { | |
| display: "flex", | |
| gap: 8, | |
| marginTop: 6, | |
| }, | |
| input: { | |
| flex: 1, | |
| padding: "12px 14px", | |
| borderRadius: 12, | |
| border: "1px solid rgba(255,255,255,0.12)", | |
| background: "#0a1026", | |
| color: "#e5e7eb", | |
| outline: "none", | |
| }, | |
| primaryBtn: { | |
| padding: "12px 14px", | |
| borderRadius: 12, | |
| border: "1px solid rgba(255,255,255,0.12)", | |
| background: "#1e40af", | |
| color: "white", | |
| fontWeight: 600, | |
| cursor: "pointer", | |
| }, | |
| ghostBtn: { | |
| padding: "8px 10px", | |
| borderRadius: 10, | |
| border: "1px solid rgba(255,255,255,0.12)", | |
| background: "transparent", | |
| color: "#e5e7eb", | |
| cursor: "pointer", | |
| }, | |
| toolbar: { | |
| marginTop: 12, | |
| marginBottom: 8, | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "space-between", | |
| gap: 8, | |
| }, | |
| chip: { | |
| padding: "8px 12px", | |
| borderRadius: 999, | |
| border: "1px solid rgba(255,255,255,0.12)", | |
| background: "transparent", | |
| color: "#e5e7eb", | |
| cursor: "pointer", | |
| }, | |
| chipActive: { | |
| background: "#1e293b", | |
| }, | |
| list: { | |
| listStyle: "none", | |
| margin: 0, | |
| padding: 0, | |
| display: "flex", | |
| flexDirection: "column", | |
| gap: 8, | |
| }, | |
| item: { | |
| display: "grid", | |
| gridTemplateColumns: "28px 1fr auto", | |
| alignItems: "center", | |
| gap: 10, | |
| padding: 12, | |
| borderRadius: 12, | |
| background: "#0a132f", | |
| border: "1px solid rgba(255,255,255,0.06)", | |
| }, | |
| checkboxLabel: { | |
| display: "inline-flex", | |
| width: 20, | |
| height: 20, | |
| alignItems: "center", | |
| justifyContent: "center", | |
| borderRadius: 6, | |
| background: "#0b173a", | |
| border: "1px solid rgba(255,255,255,0.12)", | |
| }, | |
| text: { | |
| fontSize: 16, | |
| lineHeight: 1.4, | |
| }, | |
| editInput: { | |
| padding: "8px 10px", | |
| borderRadius: 8, | |
| border: "1px solid rgba(255,255,255,0.12)", | |
| background: "#0b122c", | |
| color: "#e5e7eb", | |
| outline: "none", | |
| }, | |
| itemBtns: { | |
| display: "flex", | |
| gap: 6, | |
| }, | |
| iconBtn: { | |
| padding: "6px 8px", | |
| borderRadius: 8, | |
| border: "1px solid rgba(255,255,255,0.12)", | |
| background: "transparent", | |
| color: "#e5e7eb", | |
| cursor: "pointer", | |
| }, | |
| footer: { | |
| marginTop: 16, | |
| opacity: 0.6, | |
| fontSize: 14, | |
| }, | |
| }; |
Author
dimasusername
commented
Oct 2, 2025
Author
supports double-click editing as well
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment