From 3d93fc05e8070fdc98fc9d7297a9984afef575ae Mon Sep 17 00:00:00 2001 From: Kaustav Halder Date: Mon, 25 May 2026 22:39:51 +0530 Subject: [PATCH] Add copy and export functionality for comparison table --- frontend/package-lock.json | 51 ++++ frontend/package.json | 3 +- .../components/Features/ComparisonTable.tsx | 217 ++++++++++++++++++ .../src/components/Features/FeaturesPage.tsx | 70 +----- 4 files changed, 272 insertions(+), 69 deletions(-) create mode 100644 frontend/src/components/Features/ComparisonTable.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b88b9b0..82baec6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "clsx": "^2.1.1", "emoji-mart": "^5.6.0", "emoji-picker-react": "^4.12.3", + "html2canvas-pro": "^2.0.3", "lenis": "^1.3.8", "lucide-react": "^0.525.0", "next": "15.3.8", @@ -1836,6 +1837,15 @@ "node": ">=10" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1964,6 +1974,15 @@ "node": ">=18" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2085,6 +2104,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2126,6 +2146,19 @@ "react-is": "^16.7.0" } }, + "node_modules/html2canvas-pro": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-2.0.3.tgz", + "integrity": "sha512-lCo9YarLwEonAtH7mrh0yW0LKgpPTZFmHiqSJY/ftpBIcFhzry8LT+nBWRjbS2lgDPjS8s3PKpVRds6W3iXgcQ==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -3085,6 +3118,15 @@ "node": ">=18" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3186,6 +3228,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4c8fef1..2e24859 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "clsx": "^2.1.1", "emoji-mart": "^5.6.0", "emoji-picker-react": "^4.12.3", + "html2canvas-pro": "^2.0.3", "lenis": "^1.3.8", "lucide-react": "^0.525.0", "next": "15.3.8", @@ -43,4 +44,4 @@ "tw-animate-css": "^1.3.6", "typescript": "5.9.2" } -} \ No newline at end of file +} diff --git a/frontend/src/components/Features/ComparisonTable.tsx b/frontend/src/components/Features/ComparisonTable.tsx new file mode 100644 index 0000000..c46d383 --- /dev/null +++ b/frontend/src/components/Features/ComparisonTable.tsx @@ -0,0 +1,217 @@ +"use client" + +import { useState, useRef, useCallback } from "react" +import { Copy, Image, FileSpreadsheet, Check, ChevronDown } from "lucide-react" + +const comparisonData = [ + { feature: "Code Editor", npmchat: true, slack: false, discord: false }, + { feature: "Code Execution", npmchat: true, slack: false, discord: false }, + { feature: "Interview Mode", npmchat: true, slack: false, discord: false }, + { feature: "Open Source", npmchat: true, slack: false, discord: false }, +] + +type ExportFormat = "markdown" | "csv" | "image" + +export default function ComparisonTable() { + const [copiedFormat, setCopiedFormat] = useState(null) + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const tableRef = useRef(null) + const dropdownRef = useRef(null) + + const showCopiedFeedback = useCallback((format: ExportFormat) => { + setCopiedFormat(format) + setTimeout(() => setCopiedFormat(null), 2000) + }, []) + + const generateMarkdown = useCallback(() => { + const header = "| Feature | NPMChat | Slack | Discord |" + const separator = "|---------|---------|-------|---------|" + const rows = comparisonData.map( + (row) => + `| ${row.feature} | ${row.npmchat ? "✓" : "✗"} | ${row.slack ? "✓" : "✗"} | ${row.discord ? "✓" : "✗"} |` + ) + return [header, separator, ...rows].join("\n") + }, []) + + const generateCSV = useCallback(() => { + const header = "Feature,NPMChat,Slack,Discord" + const rows = comparisonData.map( + (row) => + `${row.feature},${row.npmchat ? "Yes" : "No"},${row.slack ? "Yes" : "No"},${row.discord ? "Yes" : "No"}` + ) + return [header, ...rows].join("\n") + }, []) + + const copyAsMarkdown = useCallback(async () => { + try { + await navigator.clipboard.writeText(generateMarkdown()) + showCopiedFeedback("markdown") + } catch { + // Fallback for older browsers + const textarea = document.createElement("textarea") + textarea.value = generateMarkdown() + document.body.appendChild(textarea) + textarea.select() + document.execCommand("copy") + document.body.removeChild(textarea) + showCopiedFeedback("markdown") + } + setIsDropdownOpen(false) + }, [generateMarkdown, showCopiedFeedback]) + + const exportAsCSV = useCallback(() => { + const csvContent = generateCSV() + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }) + const url = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = url + link.download = "npmchat-comparison.csv" + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + showCopiedFeedback("csv") + setIsDropdownOpen(false) + }, [generateCSV, showCopiedFeedback]) + + const exportAsImage = useCallback(async () => { + if (!tableRef.current) return + try { + const html2canvas = (await import("html2canvas-pro")).default + const canvas = await html2canvas(tableRef.current, { + backgroundColor: "#ffffff", + scale: 2, + useCORS: true, + }) + const url = canvas.toDataURL("image/png") + const link = document.createElement("a") + link.href = url + link.download = "npmchat-comparison.png" + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + showCopiedFeedback("image") + } catch (err) { + console.error("Failed to export as image:", err) + } + setIsDropdownOpen(false) + }, [showCopiedFeedback]) + + const exportActions: { + format: ExportFormat + label: string + icon: React.ReactNode + action: () => void + }[] = [ + { + format: "markdown", + label: "Copy as Markdown", + icon: , + action: copyAsMarkdown, + }, + { + format: "csv", + label: "Export as CSV", + icon: , + action: exportAsCSV, + }, + { + format: "image", + label: "Export as Image", + icon: , + action: exportAsImage, + }, + ] + + return ( +
+ {/* Export Controls */} +
+
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {exportActions.map(({ format, label, icon, action }) => ( + + ))} +
+ )} +
+
+ + {/* Comparison Table */} +
+
+
+ {/* Header Row */} +
+ Feature +
+
+ NPMChat +
+
+ Slack +
+
+ Discord +
+ + {/* Data Rows */} + {comparisonData.map((row) => ( +
+
+ {row.feature} +
+
+ {row.npmchat ? "✓" : "✗"} +
+
+ {row.slack ? "✓" : "✗"} +
+
+ {row.discord ? "✓" : "✗"} +
+
+ ))} +
+
+
+
+ ) +} diff --git a/frontend/src/components/Features/FeaturesPage.tsx b/frontend/src/components/Features/FeaturesPage.tsx index 1e191ee..c57be1d 100644 --- a/frontend/src/components/Features/FeaturesPage.tsx +++ b/frontend/src/components/Features/FeaturesPage.tsx @@ -1,6 +1,7 @@ import Header from "../../components/Home/Header" import Footer from "../../components/Home/Footer" import FeatureCard from "../../components/Features/FeatureCard" +import ComparisonTable from "../../components/Features/ComparisonTable" import { MessageSquare, Code2, @@ -237,74 +238,7 @@ export default function FeaturesPage() {

See what makes us different

-
-
-
- Feature -
-
- NPMChat -
-
- Slack -
-
- Discord -
- -
- Code Editor -
-
- ✓ -
-
- ✗ -
-
- ✗ -
- -
- Code Execution -
-
- ✓ -
-
- ✗ -
-
- ✗ -
- -
- Interview Mode -
-
- ✓ -
-
- ✗ -
-
- ✗ -
- -
- Open Source -
-
- ✓ -
-
- ✗ -
-
- ✗ -
-
-
+