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