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 (
setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") addItem();
}}
/>
{[
{ id: "all", label: "All" },
{ id: "active", label: "Active" },
{ id: "completed", label: "Completed" },
].map(({ id, label }) => (
))}
{left} left
);
}
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,
},
};