Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ adding export to html feature #36

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions components/home/nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,27 @@ import { Button } from '@/components/ui/button';
import Image from 'next/image';
import { GitHub } from '@/components/icons/github';
import { Sponsor } from '@/components/icons/sponsor';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import { DownloadIcon } from 'lucide-react';
import { exportHTML } from '@/lib/utils';
import { ThemeToggle } from '@/components/ui/theme-toggle';

interface NavBarProps {
suffix?: string;
cta?: string;
exportReport?: 'html' | undefined;
showFeedback?: boolean;
}

export const NavBar = ({
suffix,
cta,
exportReport,
showFeedback,
}: NavBarProps): JSX.Element => {
const [isScrolled, setIsScrolled] = useState(false);
Expand Down Expand Up @@ -70,6 +80,21 @@ export const NavBar = ({
<Sponsor />
</Button>
</Link>
{exportReport && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm'>
<DownloadIcon className='mr-2 h-4 w-4' />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={exportHTML}>
Export HTML
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{showFeedback && (
<Link
href='https://github.com/WasiqB/ultra-reporter-app/discussions/new/choose'
Expand Down
99 changes: 99 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,105 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import JSZip from 'jszip';
import FileSaver from 'file-saver';

export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}

export const exportHTML = async (): Promise<void> => {
const zip = new JSZip();

// Wait for any animations or transitions to complete
await new Promise((resolve) => setTimeout(resolve, 1000));

// Capture the current state of the DOM
const content = document.querySelector('#content');
if (!content) {
console.error('Content element not found');
return;
}

// Create a new HTML document
const doc = document.implementation.createHTMLDocument('Exported Report');

// Copy the <head> content
doc.head.innerHTML = document.head.innerHTML;

// Remove unnecessary tags from the head
const tagsToRemove = [
'script',
'noscript',
'link[rel="preload"]',
'link[rel="preconnect"]',
'link[rel="dns-prefetch"]',
];
tagsToRemove.forEach((selector) => {
doc.head.querySelectorAll(selector).forEach((el) => el.remove());
});

// Inline all stylesheets
const styles = doc.head.querySelectorAll('link[rel="stylesheet"]');
for (const style of styles) {
if (style instanceof HTMLLinkElement && style.href) {
try {
const response = await fetch(style.href);
const css = await response.text();
const inlineStyle = doc.createElement('style');
inlineStyle.textContent = css;
style.parentNode?.replaceChild(inlineStyle, style);
} catch (error) {
console.error(`Failed to fetch stylesheet: ${style.href}`, error);
}
}
}

// Copy the content
doc.body.innerHTML = `
<div id="content">
${content.innerHTML}
</div>
`;

// Remove interactive elements from the NavBar
const navbarButtons = doc.body.querySelectorAll('nav button');
navbarButtons.forEach((button) => button.remove());

// Process images
const images = doc.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.src) {
try {
const response = await fetch(img.src);
const blob = await response.blob();
const imgFileName = `images/image${i}.${blob.type.split('/')[1]}`;
zip.file(imgFileName, blob);
img.src = imgFileName;
} catch (error) {
console.error(`Failed to fetch image: ${img.src}`, error);
}
}
}

// Add necessary scripts
const scriptContent = `
// Add any necessary JavaScript here
document.addEventListener('DOMContentLoaded', function() {
// Initialize any components or functionality here
});
`;
const scriptTag = doc.createElement('script');
scriptTag.textContent = scriptContent;
doc.body.appendChild(scriptTag);

// Get the final HTML content
const finalHtmlContent = `<!DOCTYPE html>${doc.documentElement.outerHTML}`;

// Add the HTML file to the zip
zip.file('report.html', finalHtmlContent);

// Generate the zip file
const zipContent = await zip.generateAsync({ type: 'blob' });
FileSaver.saveAs(zipContent, 'report.zip');
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.447.0",
"luxon": "^3.5.0",
"next": "14.2.14",
Expand All @@ -61,6 +63,7 @@
"@stylistic/eslint-plugin-js": "^2.9.0",
"@stylistic/eslint-plugin-ts": "^2.9.0",
"@types/luxon": "^3.4.2",
"@types/file-saver": "^2.0.7",
"@types/node": "^22.7.5",
"@types/react": "^18.3.11",
"@types/react-dom": "^18",
Expand Down
Loading