Skip to content

Instantly share code, notes, and snippets.

@dimasusername
Created October 2, 2025 17:15
Show Gist options
  • Select an option

  • Save dimasusername/e98d55cd1cc5c25f3fd78650fc76d280 to your computer and use it in GitHub Desktop.

Select an option

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`
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,
},
};
@dimasusername
Copy link
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