import { useState, useEffect, useRef } from "react";
// ── Supply templates by grade ──────────────────────────────────────────────
const GRADE_TEMPLATES = {
"Kindergarten": {
essential: [
"24-count crayons","Blunt-tip scissors","Elmer's glue sticks (4-pack)","Wide-ruled composition notebook","2-pocket folders (5)","Box of tissues","Zip-top pencil pouch","Change of clothes (in zip bag)","Backpack (no wheels)","Watercolor paint set",
],
niceToHave: [
"Dry-erase markers","Play-Doh set","Sticker reward sheets","Name label stickers","Washable markers","Small hand sanitizer","Headphones (kid-safe volume limit)",
],
},
"Grade 1": {
essential: [
"#2 pencils (12-pack)","Pink eraser","24-count crayons","Blunt scissors","Glue sticks (4)","Wide-ruled spiral notebook (3)","2-pocket folders (5)","Ruler (12 in)","Clorox wipes","Box of tissues",
],
niceToHave: [
"Colored pencils","Pencil sharpener (with lid)","Dry-erase markers","Reusable water bottle","Headphones","Small whiteboard","Sticky notes",
],
},
"Grade 2": {
essential: [
"#2 pencils (24-pack)","Colored pencils (12)","Crayons (24)","Scissors","Glue sticks (4)","Wide-ruled spiral notebooks (4)","2-pocket folders (6)","12-inch ruler","Box of tissues","Clorox wipes",
],
niceToHave: [
"Markers (washable)","Pencil box","Highlighters","Sticky notes","Dry-erase markers","Mini hand sanitizer","Reusable water bottle",
],
},
"Grade 3": {
essential: [
"#2 pencils (24-pack)","Colored pencils","Scissors","Glue sticks","Wide-ruled notebooks (4)","3-ring binder (1-inch)","Divider tabs","2-pocket folders (4)","Pencil pouch","Box of tissues",
],
niceToHave: [
"Highlighters (4-color)","Sticky notes","Dry-erase markers","Ruler + protractor","Dictionary","Pencil sharpener","Reusable water bottle",
],
},
"Grade 4": {
essential: [
"#2 pencils","Colored pencils","Scissors","Glue sticks","Wide-ruled notebooks (5)","1-inch binders (2)","Dividers","Folder set","Pencil pouch","Compass & protractor",
],
niceToHave: [
"Highlighters","Sticky notes","Correction tape","Index cards","Graph paper","Ruler","Calculator (basic)",
],
},
"Grade 5": {
essential: [
"#2 pencils & pens (blue/black)","Colored pencils","Scissors","Wide-ruled notebooks (5)","1.5-inch binder","Dividers (8-tab)","Folders (5)","Pencil pouch","Scientific calculator","Ruler & protractor",
],
niceToHave: [
"Highlighters (multi-color)","Sticky notes","Index cards","Correction tape","USB flash drive","Color markers","Dry-erase markers",
],
},
"Grade 6": {
essential: [
"Blue & black pens","#2 pencils","Colored pencils","College-ruled notebooks (5)","1.5-inch binders (2)","Dividers","Folders (5)","Pencil pouch","Scientific calculator","Combination lock",
],
niceToHave: [
"Highlighters","Sticky notes","Index cards","USB flash drive","Headphones","Agenda/planner","Correction tape","Ruler",
],
},
"Grade 7": {
essential: [
"Pens (blue/black/red)","Pencils","College-ruled notebooks (5)","2-inch binder","Dividers","Folders (5)","Pencil case","Scientific calculator","Ruler","Combination lock",
],
niceToHave: [
"Highlighters","Sticky notes","USB flash drive","Agenda/planner","Index cards","Colored pens","Correction fluid","Mini stapler",
],
},
"Grade 8": {
essential: [
"Pens (blue/black)","Pencils","College-ruled notebooks (6)","2-inch binders (2)","Dividers (10-tab)","Folders","Pencil case","Scientific calculator","Ruler & compass","Combination lock",
],
niceToHave: [
"Highlighters","Sticky notes","USB flash drive","Planner/agenda","Index cards","Graph paper notebook","Correction tape","Earbuds",
],
},
"Grade 9": {
essential: [
"Pens (black/blue)","Pencils & erasers","College-ruled notebooks (6)","3-ring binders (2)","Dividers","Folders","Pencil case","TI-30X IIS calculator","Ruler","Combination lock",
],
niceToHave: [
"Highlighters (4)","Sticky notes","USB flash drive","Planner","Index cards","Graph paper","Mini stapler","Earbuds",
],
},
"Grade 10": {
essential: [
"Pens (blue/black/red)","Pencils & erasers","Notebooks (6)","3-ring binders (2)","Dividers","Folders","Pencil case","TI-84 calculator","Ruler & compass","Combination lock",
],
niceToHave: [
"Highlighters","Sticky notes","USB flash drive","Academic planner","Graph paper notebook","Protractor","Mini stapler","Earbuds",
],
},
"Grade 11": {
essential: [
"Pens","Pencils","College-ruled notebooks (6)","Binders (3)","Dividers","Folders","Pencil case","TI-84 calculator","Ruler","Flash drive (16 GB+)",
],
niceToHave: [
"Planner/agenda","Highlighters","Sticky notes","Index cards","Graph notebook","Correction tape","Mini stapler","Earbuds",
],
},
"Grade 12": {
essential: [
"Pens (blue/black)","Pencils","Notebooks (6)","Binders (3)","Dividers","Folders","Pencil case","TI-84 calculator","Flash drive (32 GB+)","Planner/agenda",
],
niceToHave: [
"Highlighters","Sticky notes","Index cards","Mini stapler","Earbuds/headphones","Ruler","Graph paper","Sticky tabs","Portfolio folder",
],
},
};
const GRADES = Object.keys(GRADE_TEMPLATES);
// ── Helpers ────────────────────────────────────────────────────────────────
function genKey() {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let k = "";
for (let i = 0; i < 8; i++) {
if (i === 4) k += "-";
k += chars[Math.floor(Math.random() * chars.length)];
}
return k;
}
function saveToStorage(key, data) {
try { localStorage.setItem("ssl_" + key, JSON.stringify(data)); return true; }
catch { return false; }
}
function loadFromStorage(key) {
try { const d = localStorage.getItem("ssl_" + key); return d ? JSON.parse(d) : null; }
catch { return null; }
}
function getAllLists() {
try {
return Object.keys(localStorage)
.filter(k => k.startsWith("ssl_"))
.map(k => ({ key: k.replace("ssl_", ""), ...JSON.parse(localStorage.getItem(k)) }));
} catch { return []; }
}
// ── QR Code (simple SVG via QR-style encoding, uses qrcode.js CDN-free fallback) ──
function QRDisplay({ value }) {
const [svg, setSvg] = useState("");
useEffect(() => {
// Simple URL-encode the key into a Google Charts QR for display
// We'll use a canvas-based approach with a tiny built-in QR
setSvg(`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(value)}&bgcolor=fff8f0&color=1a1a2e`);
}, [value]);
return svg ?
: null;
}
// ── PDF Generation ─────────────────────────────────────────────────────────
function generatePDF(list) {
const { studentName, grade, school, year, items, publishKey } = list;
const essentials = items.filter(i => i.type === "essential" && i.checked);
const extras = items.filter(i => i.type === "niceToHave" && i.checked);
const custom = items.filter(i => i.type === "custom" && i.checked);
const html = `
${studentName || "Student"} – Supply List
${studentName || "Student"}'s Supply List
${grade}
${essentials.length ? `✅ Must-Have Essentials
${essentials.map(i=>``).join("")}` : ""}
${extras.length ? `⭐ Nice to Have
${extras.map(i=>``).join("")}` : ""}
${custom.length ? `➕ Custom Items
${custom.map(i=>``).join("")}` : ""}
`;
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${(studentName || "supply-list").replace(/\s+/g, "-")}-${grade}.html`;
a.click();
URL.revokeObjectURL(url);
}
// ── Main App ───────────────────────────────────────────────────────────────
export default function App() {
const [screen, setScreen] = useState("home"); // home | editor | publish | load | view
const [lists, setLists] = useState(getAllLists);
const [activeList, setActiveList] = useState(null);
const [loadKey, setLoadKey] = useState("");
const [loadError, setLoadError] = useState("");
const [toast, setToast] = useState("");
const [newItemText, setNewItemText] = useState("");
const [showQR, setShowQR] = useState(false);
const [copiedKey, setCopiedKey] = useState(false);
function showToast(msg) {
setToast(msg);
setTimeout(() => setToast(""), 2500);
}
function createNewList() {
const grade = GRADES[0];
const template = GRADE_TEMPLATES[grade];
const items = [
...template.essential.map((name,i) => ({ id:`e${i}`, name, type:"essential", checked:true })),
...template.niceToHave.map((name,i) => ({ id:`n${i}`, name, type:"niceToHave", checked:false })),
];
const list = {
studentName:"", grade, school:"", year: new Date().getFullYear().toString(),
items, publishKey: genKey(), published: false, createdAt: Date.now(),
};
setActiveList(list);
setScreen("editor");
}
function openList(key) {
const data = loadFromStorage(key);
if (data) { setActiveList({ key, ...data }); setScreen("editor"); }
}
function updateActiveField(field, val) {
setActiveList(prev => ({ ...prev, [field]: val }));
}
function changeGrade(grade) {
const template = GRADE_TEMPLATES[grade];
const items = [
...template.essential.map((name,i) => ({ id:`e${i}`, name, type:"essential", checked:true })),
...template.niceToHave.map((name,i) => ({ id:`n${i}`, name, type:"niceToHave", checked:false })),
];
setActiveList(prev => ({
...prev, grade, items,
// keep custom items
}));
}
function toggleItem(id) {
setActiveList(prev => ({
...prev,
items: prev.items.map(i => i.id === id ? { ...i, checked: !i.checked } : i)
}));
}
function removeItem(id) {
setActiveList(prev => ({ ...prev, items: prev.items.filter(i => i.id !== id) }));
}
function addCustomItem() {
if (!newItemText.trim()) return;
const id = "c" + Date.now();
setActiveList(prev => ({
...prev,
items: [...prev.items, { id, name: newItemText.trim(), type:"custom", checked:true }]
}));
setNewItemText("");
}
function saveList() {
const key = activeList.key || activeList.publishKey;
const data = { ...activeList };
delete data.key;
saveToStorage(key, data);
setLists(getAllLists());
showToast("List saved!");
}
function publishList() {
const key = activeList.key || activeList.publishKey;
const data = { ...activeList, published: true };
delete data.key;
saveToStorage(key, data);
setActiveList({ ...data, key });
setLists(getAllLists());
setScreen("publish");
}
function handleLoadKey() {
const k = loadKey.trim().toUpperCase();
const data = loadFromStorage(k);
if (data) {
setActiveList({ key: k, ...data });
setScreen("view");
setLoadError("");
} else {
setLoadError("No list found with that key. Check the key and try again.");
}
}
function deleteList(key) {
localStorage.removeItem("ssl_" + key);
setLists(getAllLists());
}
// ── Styles ───────────────────────────────────────────────────────────────
const S = {
app: { minHeight:"100vh", background:"#faf7f4", fontFamily:"'Inter',sans-serif", color:"#1a1a2e" },
nav: { background:"#1a1a2e", padding:"0 32px", display:"flex", alignItems:"center", justifyContent:"space-between", height:60, position:"sticky", top:0, zIndex:100, boxShadow:"0 2px 12px rgba(0,0,0,.15)" },
navLogo: { fontFamily:"'Georgia',serif", fontSize:22, color:"#f4845f", fontWeight:700, cursor:"pointer", letterSpacing:"-0.5px" },
navBtn: { background:"none", border:"1.5px solid rgba(255,255,255,.25)", color:"#fff", padding:"6px 16px", borderRadius:20, cursor:"pointer", fontSize:13, fontWeight:500 },
navBtnPrimary: { background:"#f4845f", border:"none", color:"#fff", padding:"6px 16px", borderRadius:20, cursor:"pointer", fontSize:13, fontWeight:600 },
hero: { background:"linear-gradient(135deg,#1a1a2e 0%,#2d2b55 100%)", padding:"80px 32px 100px", textAlign:"center", position:"relative", overflow:"hidden" },
heroEyebrow: { color:"#f4845f", fontSize:12, fontWeight:700, letterSpacing:3, textTransform:"uppercase", marginBottom:16 },
heroTitle: { fontFamily:"'Georgia',serif", fontSize:56, color:"#fff", lineHeight:1.1, marginBottom:20, fontWeight:400 },
heroSub: { color:"rgba(255,255,255,.65)", fontSize:18, maxWidth:480, margin:"0 auto 40px" },
heroBtns: { display:"flex", gap:14, justifyContent:"center", flexWrap:"wrap" },
btnPrimary: { background:"#f4845f", color:"#fff", border:"none", padding:"14px 32px", borderRadius:30, fontSize:16, fontWeight:700, cursor:"pointer", boxShadow:"0 4px 20px rgba(244,132,95,.4)" },
btnSecondary: { background:"rgba(255,255,255,.12)", color:"#fff", border:"1.5px solid rgba(255,255,255,.3)", padding:"14px 32px", borderRadius:30, fontSize:16, fontWeight:600, cursor:"pointer" },
btnOutline: { background:"#fff", color:"#1a1a2e", border:"1.5px solid #e8e0d5", padding:"10px 22px", borderRadius:24, fontSize:14, fontWeight:600, cursor:"pointer" },
btnDanger: { background:"none", color:"#e05252", border:"1.5px solid #e8d5d5", padding:"6px 14px", borderRadius:20, cursor:"pointer", fontSize:13 },
section: { maxWidth:760, margin:"0 auto", padding:"48px 24px" },
card: { background:"#fff", borderRadius:16, border:"1px solid #ede8e2", padding:"28px", marginBottom:20, boxShadow:"0 2px 12px rgba(0,0,0,.04)" },
label: { fontSize:11, fontWeight:700, letterSpacing:2, textTransform:"uppercase", color:"#f4845f", marginBottom:6, display:"block" },
input: { width:"100%", padding:"12px 16px", border:"1.5px solid #e8e0d5", borderRadius:10, fontSize:15, fontFamily:"inherit", color:"#1a1a2e", background:"#fdfcfb", outline:"none" },
select: { width:"100%", padding:"12px 16px", border:"1.5px solid #e8e0d5", borderRadius:10, fontSize:15, fontFamily:"inherit", color:"#1a1a2e", background:"#fdfcfb", appearance:"none" },
sectionTitle: { fontSize:11, fontWeight:700, letterSpacing:2, textTransform:"uppercase", color:"#888", marginBottom:12, display:"flex", alignItems:"center", gap:8 },
itemRow: { display:"flex", alignItems:"center", gap:12, padding:"10px 0", borderBottom:"1px solid #f5f0eb" },
checkbox: { width:18, height:18, accentColor:"#f4845f", cursor:"pointer", flexShrink:0 },
itemName: { flex:1, fontSize:14 },
removeBtn: { background:"none", border:"none", color:"#ccc", cursor:"pointer", fontSize:16, lineHeight:1 },
keyDisplay: { fontFamily:"monospace", fontSize:38, fontWeight:800, letterSpacing:8, color:"#1a1a2e", textAlign:"center", background:"#fff8f0", border:"2px dashed #f4c5a8", borderRadius:14, padding:"24px 32px", marginBottom:20 },
pill: { display:"inline-block", padding:"3px 12px", borderRadius:20, fontSize:11, fontWeight:700, letterSpacing:1 },
toast: { position:"fixed", bottom:28, left:"50%", transform:"translateX(-50%)", background:"#1a1a2e", color:"#fff", padding:"12px 28px", borderRadius:30, fontSize:14, fontWeight:600, boxShadow:"0 4px 24px rgba(0,0,0,.25)", zIndex:999, whiteSpace:"nowrap" },
};
const renderEssentialDot = (type) => {
if (type === "essential") return Essential;
if (type === "niceToHave") return Nice to have;
return Custom;
};
// ── HOME ─────────────────────────────────────────────────────────────────
if (screen === "home") return (
{/* decorative circles */}
{[{top:-60,right:-60,size:280,op:.08},{bottom:-80,left:-80,size:320,op:.06}].map((c,i)=>
)}
Back to School · Grades K – 12
Every supply.
Every student.
One list.
Build a personalized supply list from grade templates, share it with a key or QR code, and download a print-ready PDF.
{/* How it works */}