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 ? QR Code : 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
${year || new Date().getFullYear()} School Year
${school || ""}

${studentName || "Student"}'s Supply List

${grade} ${essentials.length ? `
✅ Must-Have Essentials
${essentials.map(i=>`
${i.name}
`).join("")}` : ""} ${extras.length ? `
⭐ Nice to Have
${extras.map(i=>`
${i.name}
`).join("")}` : ""} ${custom.length ? `
➕ Custom Items
${custom.map(i=>`
${i.name}
`).join("")}` : ""}
Access Key
${publishKey}
`; 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 */}

How it works