Science Fair Project Planner

Science Fair Project Planner

Project Details

Hypothesis & Variables

Materials & Budget

$
Item Qty Unit Cost Total Cost Action
Total Budget: $0.00

Experimental Procedure

Results & Conclusion

No procedure steps added yet.

'; } else { sfpp_projectData.procedure.forEach((item, index) => { const stepHTML = `
${index + 1}. ${sfpp_escapeHTML(item.step)}
`; sfpp_procedureList.innerHTML += stepHTML; }); } } /** * Deletes a procedure step by its ID */ window.sfpp_deleteStep = function(id) { sfpp_projectData.procedure = sfpp_projectData.procedure.filter(item => item.id !== id); sfpp_renderProcedureList(); } // --- PDF GENERATION --- /** * Generates and downloads the project plan as a PDF */ window.sfpp_generatePdf = function() { // Check if libraries are loaded if (typeof jspdf === 'undefined' || typeof jspdf.jsPDF === 'undefined') { alert('Error: PDF generation library (jsPDF) not loaded.'); return; } if (typeof window.jspdf.plugin.autotable === 'undefined') { alert('Error: PDF generation library (jsPDF-AutoTable) not loaded.'); return; } try { const { jsPDF } = jspdf; const doc = new jsPDF(); const margin = 15; const docWidth = doc.internal.pageSize.getWidth(); const textWidth = docWidth - (margin * 2); let cursorY = 20; // --- Helper Function --- const addSection = (title, content) => { if (content && content.trim() !== "") { if (cursorY > 260) { // Check for page break doc.addPage(); cursorY = 20; } doc.setFont('helvetica', 'bold'); doc.setFontSize(14); doc.setTextColor(varGet('--sfpp-primary-color', '#0073e6')); doc.text(title, margin, cursorY); cursorY += 8; doc.setFont('helvetica', 'normal'); doc.setFontSize(11); doc.setTextColor(varGet('--sfpp-text-color', '#333')); const splitContent = doc.splitTextToSize(content, textWidth); doc.text(splitContent, margin, cursorY); cursorY += (splitContent.length * 5) + 8; // Adjust cursor } } // --- 1. Title --- doc.setFont('helvetica', 'bold'); doc.setFontSize(20); doc.setTextColor(varGet('--sfpp-primary-color', '#0073e6')); const title = sfpp_projectData.title.trim() || "My Science Fair Project"; const splitTitle = doc.splitTextToSize(title, textWidth); doc.text(splitTitle, docWidth / 2, cursorY, { align: 'center' }); cursorY += (splitTitle.length * 8) + 10; // --- 2. Details, Hypothesis, Variables --- addSection("Project Question", sfpp_projectData.question); addSection("Hypothesis", sfpp_projectData.hypothesis); addSection("Independent Variable", sfpp_projectData.variables.independent); addSection("Dependent Variable", sfpp_projectData.variables.dependent); addSection("Controlled Variables", sfpp_projectData.variables.controlled); // --- 3. Materials Table --- if (sfpp_projectData.materials.length > 0) { if (cursorY > 250) { doc.addPage(); cursorY = 20; } doc.setFont('helvetica', 'bold'); doc.setFontSize(14); doc.setTextColor(varGet('--sfpp-primary-color', '#0073e6')); doc.text("Materials & Budget", margin, cursorY); cursorY += 8; const tableHead = [['Item', 'Qty', 'Unit Cost ($)', 'Total Cost ($)']]; const tableBody = sfpp_projectData.materials.map(item => [ item.name, item.qty, item.cost.toFixed(2), (item.qty * item.cost).toFixed(2) ]); const totalBudget = sfpp_projectData.materials.reduce((sum, item) => sum + (item.qty * item.cost), 0); tableBody.push([ { content: 'Total Budget', colSpan: 3, styles: { halign: 'right', fontStyle: 'bold' } }, { content: `$${totalBudget.toFixed(2)}`, styles: { fontStyle: 'bold' } } ]); doc.autoTable({ head: tableHead, body: tableBody, startY: cursorY, theme: 'striped', headStyles: { fillColor: varGet('--sfpp-primary-color', '#0073e6'), textColor: varGet('--sfpp-text-light', '#fff') }, alternateRowStyles: { fillColor: varGet('--sfpp-light-primary', '#e6f1fc') }, styles: { font: 'helvetica', cellPadding: 2.5 } }); cursorY = doc.autoTable.previous.finalY + 10; } // --- 4. Procedure --- if (sfpp_projectData.procedure.length > 0) { if (cursorY > 260) { doc.addPage(); cursorY = 20; } doc.setFont('helvetica', 'bold'); doc.setFontSize(14); doc.setTextColor(varGet('--sfpp-primary-color', '#0073e6')); doc.text("Procedure", margin, cursorY); cursorY += 8; doc.setFont('helvetica', 'normal'); doc.setFontSize(11); doc.setTextColor(varGet('--sfpp-text-color', '#333')); sfpp_projectData.procedure.forEach((item, index) => { const stepText = `${index + 1}. ${item.step}`; const splitStep = doc.splitTextToSize(stepText, textWidth); if (cursorY + (splitStep.length * 5) > 280) { // Check for page break doc.addPage(); cursorY = 20; } doc.text(splitStep, margin, cursorY); cursorY += (splitStep.length * 5) + 3; // Line spacing for steps }); cursorY += 5; // Extra padding after section } // --- 5. Results & Conclusion --- addSection("Results & Observations", sfpp_projectData.results); addSection("Conclusion", sfpp_projectData.conclusion); // --- Save PDF --- const safeTitle = title.replace(/[^a-z0-9]/gi, '_').toLowerCase(); const fileName = `science_project_${safeTitle || 'plan'}.pdf`; doc.save(fileName); } catch (e) { console.error("PDF Generation Error: ", e); alert("An error occurred while generating the PDF."); } } // --- UTILITY FUNCTIONS --- /** * Gets a CSS variable from the root, with a fallback */ function varGet(varName, fallback = '#000') { try { return getComputedStyle(document.documentElement).getPropertyValue(varName) || fallback; } catch (e) { return fallback; } } /** * Simple HTML escaper to prevent XSS in list rendering */ function sfpp_escapeHTML(str) { if (!str) return ''; return str.replace(/[&<>"']/g, function(m) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]; }); } // --- INITIALIZATION --- function sfpp_initTool() { sfpp_switchTab(0); // Set initial tab sfpp_attachInputListeners(); // Sync form inputs to data store sfpp_renderMaterialsList(); // Render empty materials list sfpp_renderProcedureList(); // Render empty procedure list } sfpp_initTool(); // Run the tool }); // End of DOMContentLoaded