chore: update application configuration and UI components for improved styling and functionality

This commit is contained in:
Jennie Robinson Faber 2025-08-16 08:13:35 +01:00
parent 0af6b17792
commit 37ab8d7bab
54 changed files with 23293 additions and 1666 deletions

View file

@ -58,7 +58,7 @@ export const useFixtures = () => {
category: 'Services',
subcategory: 'Development',
targetPct: 65,
targetMonthlyAmount: 7800,
targetMonthlyAmount: 0,
certainty: 'Committed',
payoutDelayDays: 30,
terms: 'Net 30',
@ -73,7 +73,7 @@ export const useFixtures = () => {
category: 'Product',
subcategory: 'Digital Tools',
targetPct: 20,
targetMonthlyAmount: 2400,
targetMonthlyAmount: 0,
certainty: 'Probable',
payoutDelayDays: 14,
terms: 'Platform payout',
@ -88,7 +88,7 @@ export const useFixtures = () => {
category: 'Grant',
subcategory: 'Government',
targetPct: 10,
targetMonthlyAmount: 1200,
targetMonthlyAmount: 0,
certainty: 'Committed',
payoutDelayDays: 45,
terms: 'Quarterly disbursement',
@ -103,7 +103,7 @@ export const useFixtures = () => {
category: 'Donation',
subcategory: 'Individual',
targetPct: 3,
targetMonthlyAmount: 360,
targetMonthlyAmount: 0,
certainty: 'Aspirational',
payoutDelayDays: 3,
terms: 'Immediate',
@ -118,7 +118,7 @@ export const useFixtures = () => {
category: 'Other',
subcategory: 'Professional Services',
targetPct: 2,
targetMonthlyAmount: 240,
targetMonthlyAmount: 0,
certainty: 'Probable',
payoutDelayDays: 21,
terms: 'Net 21',
@ -163,21 +163,21 @@ export const useFixtures = () => {
{
id: 'overhead-1',
name: 'Coworking Space',
amount: 800,
amount: 0,
category: 'Workspace',
recurring: true
},
{
id: 'overhead-2',
name: 'Tools & Software',
amount: 420,
amount: 0,
category: 'Technology',
recurring: true
},
{
id: 'overhead-3',
name: 'Business Insurance',
amount: 180,
amount: 0,
category: 'Legal & Compliance',
recurring: true
}
@ -186,7 +186,7 @@ export const useFixtures = () => {
{
id: 'production-1',
name: 'Development Kits',
amount: 500,
amount: 0,
category: 'Hardware',
period: '2024-01'
}

191
composables/usePdfExport.ts Normal file
View file

@ -0,0 +1,191 @@
export const usePdfExport = () => {
const generatePDF = async (
element: HTMLElement,
filename: string = "document.pdf",
options: any = {}
): Promise<void> => {
// Default options optimized for document templates
const defaultOptions = {
margin: [0.5, 0.5, 0.5, 0.5], // top, left, bottom, right in inches
filename,
image: { type: "jpeg", quality: 0.98 },
html2canvas: {
scale: 2, // Higher scale for better quality
useCORS: true,
allowTaint: false,
letterRendering: true,
logging: false,
dpi: 300,
height: undefined,
width: undefined,
},
jsPDF: {
unit: "in",
format: "letter",
orientation: "portrait",
compress: true,
},
pagebreak: {
mode: ["avoid-all", "css", "legacy"],
before: ".page-break-before",
after: ".page-break-after",
avoid: ".no-page-break",
},
};
// Merge provided options with defaults
const finalOptions = {
...defaultOptions,
...options,
html2canvas: {
...defaultOptions.html2canvas,
...(options.html2canvas || {}),
},
jsPDF: {
...defaultOptions.jsPDF,
...(options.jsPDF || {}),
},
pagebreak: {
...defaultOptions.pagebreak,
...(options.pagebreak || {}),
},
};
try {
// Dynamic import for client-side only
if (process.server) {
throw new Error("PDF generation is only available on the client side");
}
// Import html2pdf dynamically with better error handling
let html2pdf;
try {
const module = await import("html2pdf.js");
html2pdf = module.default || module;
} catch (importError) {
console.error("Failed to import html2pdf.js:", importError);
throw new Error(
"Failed to load PDF library. Please refresh the page and try again."
);
}
// Clone the element to avoid modifying the original
const clonedElement = element.cloneNode(true) as HTMLElement;
// Apply PDF-specific styling to the clone while preserving font choices
const currentFontClass = element.className.match(/font-[\w-]+/)?.[0];
let fontFamily = '"Times New Roman", "Times", serif'; // default
// Preserve selected font for PDF
if (currentFontClass) {
if (currentFontClass.includes("source-serif")) {
fontFamily = '"Source Serif 4", "Times New Roman", serif';
} else if (currentFontClass.includes("ubuntu")) {
fontFamily =
'"Ubuntu", -apple-system, BlinkMacSystemFont, sans-serif';
} else if (currentFontClass.includes("inter")) {
fontFamily = '"Inter", -apple-system, BlinkMacSystemFont, sans-serif';
}
}
clonedElement.style.fontFamily = fontFamily;
clonedElement.style.fontSize = "11pt";
clonedElement.style.lineHeight = "1.5";
clonedElement.style.color = "#000000";
clonedElement.style.backgroundColor = "#ffffff";
clonedElement.style.width = "8.5in";
clonedElement.style.maxWidth = "8.5in";
clonedElement.style.padding = "0.5in";
clonedElement.style.boxSizing = "border-box";
// Hide export controls in the clone
const exportControls = clonedElement.querySelector(".export-controls");
if (exportControls) {
(exportControls as HTMLElement).style.display = "none";
}
// Ensure proper font loading by adding a slight delay
await new Promise((resolve) => setTimeout(resolve, 500));
// Generate the PDF with better error handling
console.log("Starting PDF generation with options:", finalOptions);
if (typeof html2pdf !== "function") {
throw new Error(
"html2pdf is not a function. Library may not have loaded correctly."
);
}
const pdfInstance = html2pdf();
if (!pdfInstance || typeof pdfInstance.set !== "function") {
throw new Error("html2pdf instance is invalid");
}
await pdfInstance.set(finalOptions).from(clonedElement).save();
} catch (error: any) {
console.error("PDF generation failed:", error);
const errorMessage =
error?.message || error?.toString() || "Unknown error";
throw new Error(`PDF generation failed: ${errorMessage}`);
}
};
const generatePDFFromContent = async (
htmlContent: string,
filename: string = "document.pdf",
options: any = {}
): Promise<void> => {
if (process.server) {
throw new Error("PDF generation is only available on the client side");
}
// Create a temporary element with the HTML content
const tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlContent;
tempDiv.style.position = "absolute";
tempDiv.style.left = "-9999px";
tempDiv.style.top = "-9999px";
tempDiv.style.width = "8.5in";
tempDiv.style.fontFamily = '"Times New Roman", "Times", serif';
tempDiv.style.fontSize = "11pt";
tempDiv.style.lineHeight = "1.5";
tempDiv.style.color = "#000000";
tempDiv.style.backgroundColor = "#ffffff";
document.body.appendChild(tempDiv);
try {
await generatePDF(tempDiv, filename, options);
} finally {
document.body.removeChild(tempDiv);
}
};
const exportDocumentAsPDF = async (
selector: string = ".document-page",
filename?: string
): Promise<void> => {
if (process.server) {
throw new Error("PDF generation is only available on the client side");
}
const element = document.querySelector(selector) as HTMLElement;
if (!element) {
throw new Error(`Element with selector "${selector}" not found`);
}
// Generate filename based on current date if not provided
const defaultFilename = `document-${
new Date().toISOString().split("T")[0]
}.pdf`;
await generatePDF(element, filename || defaultFilename);
};
return {
generatePDF,
generatePDFFromContent,
exportDocumentAsPDF,
};
};

View file

@ -0,0 +1,534 @@
export const usePdfExportBasic = () => {
const exportToPDF = async (elementSelector: string, filename: string) => {
// Only run on client side
if (process.server || typeof window === "undefined") {
throw new Error("PDF generation is only available on the client side");
}
try {
console.log("Starting professional PDF export...");
// Get the element to export
const element = document.querySelector(elementSelector);
if (!element) {
throw new Error(`Element with selector "${elementSelector}" not found`);
}
// Use jsPDF directly for better control and professional output
const { jsPDF } = await import("jspdf");
// Extract form data from localStorage (similar to membership agreement)
const savedData = localStorage.getItem("membership-agreement-data");
const formData = savedData ? JSON.parse(savedData) : {};
// Create PDF with professional styling
const pdf = new jsPDF({
orientation: "portrait",
unit: "in",
format: "letter",
});
// Helper function for page management (from revenue worksheet)
const checkPageBreak = (
currentY: number,
neededSpace: number = 0.5
): number => {
if (currentY + neededSpace > 10) {
pdf.addPage();
return 1;
}
return currentY;
};
// Helper function for section headers
const addSectionHeader = (title: string, yPos: number): number => {
// Force page break if not enough space for section header + some content
if (yPos > 8.5) {
pdf.addPage();
yPos = 1;
}
pdf.setFillColor(240, 240, 240);
pdf.rect(0.75, yPos - 0.1, 7, 0.4, "F");
pdf.setFontSize(14);
pdf.setFont("helvetica", "bold");
pdf.setTextColor(0, 0, 0);
pdf.text(title, 1, yPos + 0.15);
return yPos + 0.5;
};
// Format date helper
const formatDate = (dateStr: string) => {
if (!dateStr) return "[_____]";
return new Date(dateStr).toLocaleDateString();
};
// Header with professional styling
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, 8.5, 1.2, "F");
pdf.setFontSize(20);
pdf.setFont("helvetica", "bold");
pdf.setTextColor(255, 255, 255);
pdf.text("MEMBERSHIP AGREEMENT", 4.25, 0.6, { align: "center" });
pdf.setFontSize(12);
pdf.setFont("helvetica", "normal");
pdf.text(formData.cooperativeName || "Worker Cooperative", 4.25, 0.9, {
align: "center",
});
// Reset text color
pdf.setTextColor(0, 0, 0);
// Document info
let yPos = 1.5;
pdf.setFontSize(11);
pdf.setFont("helvetica", "normal");
const now = new Date();
const generatedDate = now.toLocaleDateString();
pdf.text(`Generated: ${generatedDate}`, 1, yPos);
yPos += 0.3;
// Helper function for consistent body text
const addBodyText = (
text: string,
yPos: number,
indent: number = 1
): number => {
pdf.setFontSize(10);
pdf.setFont("helvetica", "normal");
const lines = pdf.splitTextToSize(text, 6.5);
pdf.text(lines, indent, yPos);
return yPos + lines.length * 0.15;
};
// Helper function for subsection headers
const addSubsectionHeader = (title: string, yPos: number): number => {
yPos = checkPageBreak(yPos, 0.3);
pdf.setFontSize(11);
pdf.setFont("helvetica", "bold");
pdf.text(title, 1, yPos);
return yPos + 0.2;
};
// Helper function for bullet lists
const addBulletList = (
items: string[],
yPos: number,
indent: number = 1.2
): number => {
pdf.setFontSize(10);
pdf.setFont("helvetica", "normal");
items.forEach((item) => {
yPos = checkPageBreak(yPos, 0.2);
const lines = pdf.splitTextToSize(item, 6.5);
pdf.text(lines, indent, yPos);
yPos += lines.length * 0.18; // Increased line height
});
return yPos + 0.15; // Increased spacing after list
};
// Section 1: Who We Are
yPos = addSectionHeader("1. Who We Are", yPos);
pdf.setFontSize(10);
pdf.setFont("helvetica", "bold");
pdf.text("Date Established:", 1, yPos);
pdf.setFont("helvetica", "normal");
pdf.text(formatDate(formData.dateEstablished), 2.5, yPos);
yPos += 0.3;
pdf.setFont("helvetica", "bold");
pdf.text("Our Purpose:", 1, yPos);
yPos += 0.15;
yPos = addBodyText(formData.purpose || "[Purpose to be filled in]", yPos);
yPos += 0.15;
pdf.setFont("helvetica", "bold");
pdf.text("Our Core Values:", 1, yPos);
yPos += 0.15;
yPos = addBodyText(
formData.coreValues || "[Core values to be filled in]",
yPos
);
yPos += 0.2;
// Section 2: Membership
yPos = addSectionHeader("2. Membership", yPos);
yPos = addSubsectionHeader("Who Can Be a Member", yPos);
yPos = addBodyText("Any person who:", yPos);
yPos += 0.1;
const memberCriteria = [
"• Shares our values and purpose",
"• Contributes labour to the cooperative (by doing actual work, not just investing money)",
"• Commits to collective decision-making",
"• Participates in governance responsibilities",
];
yPos = addBulletList(memberCriteria, yPos);
yPos = addSubsectionHeader("Becoming a Member", yPos);
yPos = addBodyText(
"New members join through a consent process, which means existing members must agree that adding this person won't harm the cooperative.",
yPos
);
yPos += 0.15;
const membershipSteps = [
`1. Trial period of ${
formData.trialPeriodMonths || "[___]"
} months working together`,
"2. Values alignment conversation",
"3. Consent decision by current members",
`4. Optional - Equal buy-in contribution of $${
formData.buyInAmount || "[___]"
} (can be paid over time or waived based on need)`,
];
yPos = addBulletList(membershipSteps, yPos);
yPos = addSubsectionHeader("Leaving the Cooperative", yPos);
yPos = addBodyText(
`Members can leave anytime with ${
formData.noticeDays || "[___]"
} days notice. The cooperative will:`,
yPos
);
yPos += 0.1;
const leavingSteps = [
`• Pay out their share of any surplus within ${
formData.surplusPayoutDays || "[___]"
} days`,
`• Return their buy-in contribution within ${
formData.buyInReturnDays || "[___]"
} days`,
"• Maintain respectful ongoing relationships when possible",
];
yPos = addBulletList(leavingSteps, yPos);
yPos += 0.1;
// Section 3: How We Make Decisions
yPos = addSectionHeader("3. How We Make Decisions", yPos);
yPos = addSubsectionHeader("Consent-Based Decisions", yPos);
yPos = addBodyText(
"We use consent, not consensus. This means we move forward when no one has a principled objection that would harm the cooperative. An objection must explain how the proposal would contradict our values or threaten our sustainability.",
yPos
);
yPos += 0.15;
yPos = addSubsectionHeader("Day-to-Day Decisions", yPos);
yPos = addBodyText(
`Decisions under $${
formData.dayToDayLimit || "[___]"
} can be made by any member. Just tell others what you did at the next meeting.`,
yPos
);
yPos += 0.15;
yPos = addSubsectionHeader("Regular Decisions", yPos);
yPos = addBodyText(
`Decisions between $${formData.regularDecisionMin || "[___]"} and $${
formData.regularDecisionMax || "[___]"
} need consent from members present at a meeting (minimum 2 members).`,
yPos
);
yPos += 0.15;
yPos = addSubsectionHeader("Major Decisions", yPos);
yPos = addBodyText("These require consent from all members:", yPos);
yPos += 0.1;
const majorDecisions = [
"• Adding or removing members",
"• Changing this agreement",
`• Taking on debt over $${formData.majorDebtThreshold || "[___]"}`,
"• Fundamental changes to our purpose or structure",
"• Dissolution of the cooperative",
];
yPos = addBulletList(majorDecisions, yPos);
yPos = addSubsectionHeader("Meeting Structure", yPos);
const meetingItems = [
`• We meet ${
formData.meetingFrequency || "[___]"
} to make decisions together`,
`• Emergency meetings need ${
formData.emergencyNoticeHours || "[___]"
} hours notice`,
"• All members can add items to the agenda",
"• We keep simple records of what we decide",
];
yPos = addBulletList(meetingItems, yPos);
// Section 4: Money and Labour
yPos = addSectionHeader("4. Money and Labour", yPos);
yPos = addSubsectionHeader("Equal Ownership", yPos);
yPos = addBodyText(
"Each member owns an equal share of the cooperative, regardless of when they joined or how much money they put in.",
yPos
);
yPos += 0.15;
yPos = addSubsectionHeader("Paying Ourselves", yPos);
const paymentItems = [
`• Base hourly rate: $${
formData.baseRate || "[___]"
}/hour for all members`,
`• Monthly draw: $${
formData.monthlyDraw || "[___]"
} per month (if applicable)`,
`• Payment date: ${formData.paymentDay || "[___]"}th of each month`,
`• Surplus sharing: ${
formData.surplusFrequency || "[___]"
}ly based on hours worked`,
];
yPos = addBulletList(paymentItems, yPos);
yPos = addSubsectionHeader("Work Expectations", yPos);
const workItems = [
`• Target hours per week: ${formData.targetHours || "[___]"} hours`,
"• All work counts equally - admin, client work, business development",
"• We track hours honestly and transparently",
"• Flexible scheduling based on personal needs and business requirements",
];
yPos = addBulletList(workItems, yPos);
yPos = addSubsectionHeader("Financial Transparency", yPos);
const transparencyItems = [
"• All members can access all financial records anytime",
"• Monthly financial updates shared with everyone",
"• Annual financial review conducted together",
"• No secret salaries or hidden expenses",
];
yPos = addBulletList(transparencyItems, yPos);
// Section 5: Roles and Responsibilities
yPos = addSectionHeader("5. Roles and Responsibilities", yPos);
yPos = addSubsectionHeader("Rotating Roles", yPos);
yPos = addBodyText(
`We rotate operational roles every ${
formData.roleRotationMonths || "[___]"
} months to share knowledge and prevent burnout. Current roles include:`,
yPos
);
yPos += 0.1;
const roles = [
"• Financial coordinator (bookkeeping, invoicing, payments)",
"• Client relationship manager (main point of contact)",
"• Operations coordinator (scheduling, project management)",
"• Business development (marketing, new client outreach)",
];
yPos = addBulletList(roles, yPos);
yPos = addSubsectionHeader("Shared Responsibilities", yPos);
yPos = addBodyText("All members participate in:", yPos);
yPos += 0.1;
const sharedResponsibilities = [
"• Weekly planning and check-in meetings",
"• Monthly financial review",
"• Annual strategic planning",
"• Conflict resolution when needed",
"• Onboarding new members",
];
yPos = addBulletList(sharedResponsibilities, yPos);
// Section 6: Conflict and Care
yPos = addSectionHeader("6. Conflict and Care", yPos);
yPos = addSubsectionHeader("When Conflict Happens", yPos);
const conflictSteps = [
"1. Direct conversation between parties (if comfortable)",
"2. Bring in a neutral member as mediator",
"3. Full group conversation if needed",
"4. External mediation if we can't resolve it ourselves",
"5. As a last resort, consent process about membership",
];
yPos = addBulletList(conflictSteps, yPos);
yPos = addSubsectionHeader("Care Commitments", yPos);
const careItems = [
"• We check in about capacity and wellbeing regularly",
"• We adjust workload when someone is struggling",
"• We celebrate successes and support through failures",
"• We maintain boundaries between work and personal relationships",
"• We commit to direct, kind communication",
];
yPos = addBulletList(careItems, yPos);
// Section 7: Changing This Agreement
yPos = addSectionHeader("7. Changing This Agreement", yPos);
yPos = addBodyText(
`This agreement gets reviewed every ${
formData.reviewFrequency || "[___]"
} and can be changed anytime with consent from all members. We'll update it as we learn what works for us.`,
yPos
);
yPos += 0.2;
// Section 8: If We Need to Close
yPos = addSectionHeader("8. If We Need to Close", yPos);
yPos = addBodyText("If the cooperative dissolves:", yPos);
yPos += 0.1;
const dissolutionItems = [
"• Pay all debts and obligations first",
"• Return member buy-ins",
`• Donate remaining assets to ${
formData.assetDonationTarget || "[organization to be determined]"
}`,
"• Close all legal and financial accounts",
"• Celebrate what we built together",
];
yPos = addBulletList(dissolutionItems, yPos);
// Section 9: Legal Bits
yPos = addSectionHeader("9. Legal Bits", yPos);
pdf.setFontSize(10);
pdf.setFont("helvetica", "bold");
pdf.text("Legal Structure:", 1, yPos);
pdf.setFont("helvetica", "normal");
pdf.text(formData.legalStructure || "[To be determined]", 2.5, yPos);
yPos += 0.2;
pdf.setFont("helvetica", "bold");
pdf.text("Registered Location:", 1, yPos);
pdf.setFont("helvetica", "normal");
pdf.text(formData.registeredLocation || "[To be determined]", 2.5, yPos);
yPos += 0.2;
pdf.setFont("helvetica", "bold");
pdf.text("Fiscal Year End:", 1, yPos);
pdf.setFont("helvetica", "normal");
pdf.text(
`${formData.fiscalYearEndMonth || "December"} ${
formData.fiscalYearEndDay || "31"
}`,
2.5,
yPos
);
yPos += 0.3;
// Section 10: Agreement Review
yPos = addSectionHeader("10. Agreement Review", yPos);
yPos = addBodyText(
`Last Updated: ${formatDate(formData.lastUpdated)}`,
yPos
);
yPos += 0.1;
yPos = addBodyText(
`Next Review: ${
formatDate(formData.nextReview) || "[To be scheduled]"
}`,
yPos
);
yPos += 0.2;
// Current Members section (if any members are entered)
if (
formData.members &&
formData.members.length > 0 &&
formData.members.some((m: any) => m.name)
) {
yPos = addSectionHeader("Current Members", yPos);
formData.members.forEach((member: any, index: number) => {
if (member.name) {
yPos = checkPageBreak(yPos, 0.8);
pdf.setFontSize(11);
pdf.setFont("helvetica", "bold");
pdf.text(`${index + 1}. ${member.name}`, 1, yPos);
yPos += 0.25;
pdf.setFontSize(10);
pdf.setFont("helvetica", "normal");
if (member.email) {
pdf.text(`Email: ${member.email}`, 1.2, yPos);
yPos += 0.18;
}
if (member.joinDate) {
pdf.text(`Joined: ${formatDate(member.joinDate)}`, 1.2, yPos);
yPos += 0.18;
}
if (member.role) {
pdf.text(`Role: ${member.role}`, 1.2, yPos);
yPos += 0.18;
}
yPos += 0.25;
}
});
yPos += 0.2;
}
// Signature section
yPos = checkPageBreak(yPos, 2.5);
pdf.setFontSize(12);
pdf.setFont("helvetica", "bold");
pdf.text("Member Signatures", 1, yPos);
yPos += 0.3;
pdf.setFontSize(10);
pdf.setFont("helvetica", "normal");
pdf.text(
"By signing below, we agree to these terms and commit to working together as equals in this cooperative.",
1,
yPos
);
yPos += 0.3;
// Create signature lines based on actual members (minimum 2, maximum 8)
const membersWithNames = formData.members
? formData.members.filter((m: any) => m.name)
: [];
const numSignatureLines = Math.max(
2,
Math.min(8, membersWithNames.length || 4)
);
const signatureLineWidth = 5;
for (let i = 0; i < numSignatureLines; i++) {
yPos = checkPageBreak(yPos, 0.6);
// Pre-fill member name if available, otherwise leave blank
const memberName = membersWithNames[i]?.name || "";
if (memberName) {
pdf.setFont("helvetica", "normal");
pdf.setFontSize(10);
pdf.text(memberName, 1, yPos);
yPos += 0.2;
}
// Simple signature line (1px thin line)
pdf.setLineWidth(0.01); // Very thin line
pdf.line(1, yPos, 1 + signatureLineWidth, yPos);
pdf.setLineWidth(0.2); // Reset to default
yPos += 0.4;
}
console.log("Saving PDF...");
pdf.save(filename);
console.log("PDF saved successfully!");
} catch (error: any) {
console.error("Basic PDF generation error:", error);
throw new Error(`PDF generation failed: ${error.message || error}`);
}
};
return { exportToPDF };
};

View file

@ -0,0 +1,186 @@
export const usePdfExportSafe = () => {
const exportToPDF = async (elementSelector: string, filename: string) => {
// Only run on client side
if (process.server || typeof window === "undefined") {
throw new Error("PDF generation is only available on the client side");
}
try {
// Dynamic import for client-side only
const html2pdf = (await import("html2pdf.js")).default;
// Get the element to export
const element = document.querySelector(elementSelector);
if (!element) {
throw new Error(`Element with selector "${elementSelector}" not found`);
}
// Create a completely clean version of the content
const createCleanContent = () => {
const cleanDiv = document.createElement("div");
cleanDiv.style.cssText = `
width: 8.5in;
padding: 0.5in;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
line-height: 1.5;
color: #000000;
background: #ffffff;
`;
// Extract text content and basic structure
const extractContent = (sourceEl: Element, targetEl: HTMLElement) => {
const children = sourceEl.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
// Skip export controls
if (
child.classList.contains("export-controls") ||
child.classList.contains("no-pdf") ||
child.classList.contains("no-print")
) {
continue;
}
let newEl: HTMLElement;
// Create appropriate element based on tag
switch (child.tagName.toLowerCase()) {
case "h1":
newEl = document.createElement("h1");
newEl.style.cssText =
"font-size: 24px; font-weight: bold; margin: 20px 0 10px 0; color: #000;";
break;
case "h2":
newEl = document.createElement("h2");
newEl.style.cssText =
"font-size: 20px; font-weight: bold; margin: 16px 0 8px 0; color: #000;";
break;
case "h3":
newEl = document.createElement("h3");
newEl.style.cssText =
"font-size: 16px; font-weight: bold; margin: 12px 0 6px 0; color: #000;";
break;
case "p":
newEl = document.createElement("p");
newEl.style.cssText = "margin: 8px 0; color: #000;";
break;
case "ul":
newEl = document.createElement("ul");
newEl.style.cssText =
"margin: 8px 0; padding-left: 20px; color: #000;";
break;
case "ol":
newEl = document.createElement("ol");
newEl.style.cssText =
"margin: 8px 0; padding-left: 20px; color: #000;";
break;
case "li":
newEl = document.createElement("li");
newEl.style.cssText = "margin: 4px 0; color: #000;";
break;
case "input":
newEl = document.createElement("span");
const inputEl = child as HTMLInputElement;
newEl.textContent = inputEl.value || "[_____]";
newEl.style.cssText =
"border-bottom: 1px solid #000; padding: 2px; color: #000;";
break;
case "textarea":
newEl = document.createElement("span");
const textareaEl = child as HTMLTextAreaElement;
newEl.textContent = textareaEl.value || "[_____]";
newEl.style.cssText =
"border: 1px solid #000; padding: 4px; display: inline-block; color: #000;";
break;
case "select":
newEl = document.createElement("span");
const selectEl = child as HTMLSelectElement;
newEl.textContent = selectEl.value || "[_____]";
newEl.style.cssText =
"border: 1px solid #000; padding: 2px; color: #000;";
break;
default:
newEl = document.createElement("div");
newEl.style.cssText = "color: #000;";
}
// Set text content for elements that should have it
if (
child.tagName.toLowerCase() !== "input" &&
child.tagName.toLowerCase() !== "textarea" &&
child.tagName.toLowerCase() !== "select"
) {
// Get only direct text content, not from children
const directText = Array.from(child.childNodes)
.filter((node) => node.nodeType === Node.TEXT_NODE)
.map((node) => node.textContent)
.join("");
if (directText.trim()) {
newEl.appendChild(document.createTextNode(directText));
}
}
targetEl.appendChild(newEl);
// Recursively process children
if (child.children.length > 0) {
extractContent(child, newEl);
}
}
};
extractContent(element, cleanDiv);
return cleanDiv;
};
const cleanElement = createCleanContent();
// Temporarily add to DOM
cleanElement.style.position = "absolute";
cleanElement.style.left = "-9999px";
cleanElement.style.top = "-9999px";
document.body.appendChild(cleanElement);
// Simple options - no complex CSS processing
const options = {
margin: 0.5,
filename: filename,
image: { type: "jpeg", quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
allowTaint: false,
logging: false,
backgroundColor: "#ffffff",
},
jsPDF: {
unit: "in",
format: "letter",
orientation: "portrait",
},
};
console.log("Generating PDF with clean content...");
try {
// Generate PDF from the clean element
await html2pdf().set(options).from(cleanElement).save();
console.log("PDF generated successfully!");
} finally {
// Clean up
if (cleanElement.parentNode) {
document.body.removeChild(cleanElement);
}
}
} catch (error: any) {
console.error("PDF generation error:", error);
throw new Error(`PDF generation failed: ${error.message || error}`);
}
};
return { exportToPDF };
};

View file

@ -0,0 +1,170 @@
export const usePdfExportSimple = () => {
const exportToPDF = async (elementSelector: string, filename: string) => {
// Only run on client side
if (process.server || typeof window === "undefined") {
throw new Error("PDF generation is only available on the client side");
}
try {
// Dynamic import for client-side only
const html2pdf = (await import("html2pdf.js")).default;
// Get the element to export
const element = document.querySelector(elementSelector);
if (!element) {
throw new Error(`Element with selector "${elementSelector}" not found`);
}
// Clone the element to avoid modifying the original
const clonedElement = element.cloneNode(true) as HTMLElement;
// Fix CSS compatibility issues by applying only computed styles
const fixCSSCompatibility = (el: HTMLElement) => {
// Get all elements including the root
const allElements = [el, ...el.querySelectorAll("*")] as HTMLElement[];
allElements.forEach((elem) => {
// Get computed styles
const computedStyle = window.getComputedStyle(elem);
// Clear all existing styles to start fresh
elem.removeAttribute("style");
elem.removeAttribute("class");
// Apply only essential computed styles that are safe
elem.style.display = computedStyle.display;
elem.style.position = computedStyle.position;
elem.style.width = computedStyle.width;
elem.style.height = computedStyle.height;
elem.style.margin = computedStyle.margin;
elem.style.padding = computedStyle.padding;
elem.style.fontSize = computedStyle.fontSize;
elem.style.fontWeight = computedStyle.fontWeight;
elem.style.fontFamily = computedStyle.fontFamily;
elem.style.lineHeight = computedStyle.lineHeight;
elem.style.textAlign = computedStyle.textAlign;
// Apply safe color values - convert any complex colors to simple ones
const safeColor =
computedStyle.color.includes("oklch") ||
computedStyle.color.includes("oklab")
? "#000000"
: computedStyle.color;
const safeBgColor =
computedStyle.backgroundColor.includes("oklch") ||
computedStyle.backgroundColor.includes("oklab")
? "transparent"
: computedStyle.backgroundColor;
elem.style.color = safeColor;
elem.style.backgroundColor = safeBgColor;
elem.style.borderWidth = computedStyle.borderWidth;
elem.style.borderStyle = computedStyle.borderStyle;
elem.style.borderColor = "#cccccc"; // Safe fallback color
});
};
// Apply CSS fixes
fixCSSCompatibility(clonedElement);
// Temporarily add to DOM for processing
clonedElement.style.position = "absolute";
clonedElement.style.left = "-9999px";
clonedElement.style.top = "-9999px";
document.body.appendChild(clonedElement);
// Simple options for better compatibility
const options = {
margin: 0.5,
filename: filename,
image: { type: "jpeg", quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
allowTaint: false,
logging: false,
ignoreElements: (element: Element) => {
// Skip elements that might cause issues
return (
element.classList.contains("no-pdf") ||
element.classList.contains("export-controls")
);
},
onclone: (clonedDoc: Document) => {
// Remove ALL existing stylesheets to avoid oklch() issues
const stylesheets = clonedDoc.querySelectorAll(
'style, link[rel="stylesheet"]'
);
stylesheets.forEach((sheet) => sheet.remove());
// Add only safe, basic CSS
const safeStyle = clonedDoc.createElement("style");
safeStyle.textContent = `
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
line-height: 1.5;
color: #000000;
background: #ffffff;
}
.export-controls, .no-pdf {
display: none !important;
}
/* Basic typography */
h1 { font-size: 24px; font-weight: bold; margin: 20px 0 10px 0; }
h2 { font-size: 20px; font-weight: bold; margin: 16px 0 8px 0; }
h3 { font-size: 16px; font-weight: bold; margin: 12px 0 6px 0; }
p { margin: 8px 0; }
ul, ol { margin: 8px 0; padding-left: 20px; }
li { margin: 4px 0; }
/* Form elements */
input, textarea, select {
border: 1px solid #ccc;
padding: 4px;
font-size: 12px;
background: #fff;
color: #000;
}
`;
clonedDoc.head.appendChild(safeStyle);
},
},
jsPDF: {
unit: "in",
format: "letter",
orientation: "portrait",
},
};
console.log("Generating PDF with html2pdf...");
// Wait a moment for DOM changes to settle
await new Promise((resolve) => setTimeout(resolve, 100));
try {
// Generate and save the PDF using the cloned element
await html2pdf().set(options).from(clonedElement).save();
console.log("PDF generated successfully!");
} finally {
// Clean up the cloned element
if (clonedElement.parentNode) {
document.body.removeChild(clonedElement);
}
}
} catch (error: any) {
console.error("PDF generation error:", error);
throw new Error(`PDF generation failed: ${error.message || error}`);
}
};
return { exportToPDF };
};