chore: migrate to TypeScript

- Convert all .js files to .tsx/.ts
- Add types for data structures (MuseumRecord, Metrics, etc.)
- Add type declarations for react-chartjs-2
- Configure tsconfig with relaxed strictness for gradual adoption
- All components now use TypeScript
This commit is contained in:
fahed
2026-02-04 13:45:50 +03:00
parent e98bebd60b
commit 868f46fc6e
18 changed files with 484 additions and 121 deletions

View File

@@ -0,0 +1,184 @@
import React, { useRef, useState, ReactNode } from 'react';
import JSZip from 'jszip';
interface ExportableChartProps {
children: ReactNode;
filename?: string;
title?: string;
className?: string;
controls?: ReactNode;
}
// Wrapper component that adds PNG export to any chart
export function ExportableChart({
children,
filename = 'chart',
title = '',
className = '',
controls = null
}: ExportableChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
const exportAsPNG = () => {
const chartContainer = chartRef.current;
if (!chartContainer) return;
const canvas = chartContainer.querySelector('canvas');
if (!canvas) return;
// Create a new canvas with white background and title
const exportCanvas = document.createElement('canvas');
const ctx = exportCanvas.getContext('2d');
if (!ctx) return;
// Set dimensions with padding and title space
const padding = 24;
const titleHeight = title ? 48 : 0;
exportCanvas.width = canvas.width + (padding * 2);
exportCanvas.height = canvas.height + (padding * 2) + titleHeight;
// Fill white background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
// Draw title if provided (left-aligned, matching on-screen style)
if (title) {
ctx.fillStyle = '#1e293b';
ctx.font = '600 20px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(title, padding, padding + 24);
}
// Draw the chart
ctx.drawImage(canvas, padding, padding + titleHeight);
// Export
const link = document.createElement('a');
link.download = `${filename}-${new Date().toISOString().split('T')[0]}.png`;
link.href = exportCanvas.toDataURL('image/png', 1.0);
link.click();
};
return (
<div className="exportable-chart-wrapper">
{/* Download button - positioned absolutely in corner */}
<button
className="chart-export-btn visible"
onClick={exportAsPNG}
title="Download as PNG"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
{title && (
<div className="chart-header-with-export">
<h2>{title}</h2>
{controls && <div className="chart-header-actions">{controls}</div>}
</div>
)}
{!title && controls && <div className="chart-controls">{controls}</div>}
<div className={`exportable-chart ${className}`}>
<div ref={chartRef} className="chart-canvas-wrapper">
{children}
</div>
</div>
</div>
);
}
// Utility function to export all charts from a container as a ZIP
export async function exportAllCharts(containerSelector: string, zipFilename: string = 'charts'): Promise<void> {
const container = document.querySelector(containerSelector);
if (!container) return;
const zip = new JSZip();
const chartWrappers = container.querySelectorAll('.exportable-chart-wrapper');
for (let i = 0; i < chartWrappers.length; i++) {
const wrapper = chartWrappers[i];
const canvas = wrapper.querySelector('canvas');
const titleEl = wrapper.querySelector('.chart-header-with-export h2');
const title = titleEl?.textContent || `chart-${i + 1}`;
if (!canvas) continue;
// Create export canvas with white background and title
const exportCanvas = document.createElement('canvas');
const ctx = exportCanvas.getContext('2d');
if (!ctx) continue;
const padding = 32;
const titleHeight = 56;
exportCanvas.width = canvas.width + (padding * 2);
exportCanvas.height = canvas.height + (padding * 2) + titleHeight;
// White background
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
// Draw title
ctx.fillStyle = '#1e293b';
ctx.font = '600 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
ctx.textAlign = 'left';
ctx.fillText(title, padding, padding + 28);
// Draw chart
ctx.drawImage(canvas, padding, padding + titleHeight);
// Convert to blob and add to zip
const dataUrl = exportCanvas.toDataURL('image/png', 1.0);
const base64Data = dataUrl.split(',')[1];
const safeFilename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF\s-]/g, '').replace(/\s+/g, '-');
zip.file(`${String(i + 1).padStart(2, '0')}-${safeFilename}.png`, base64Data, { base64: true });
}
// Generate and download ZIP
const blob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${zipFilename}-${new Date().toISOString().split('T')[0]}.zip`;
link.click();
URL.revokeObjectURL(url);
}
interface ExportAllButtonProps {
containerSelector: string;
zipFilename?: string;
label: string;
loadingLabel: string;
}
// Button component for exporting all charts
export function ExportAllButton({ containerSelector, zipFilename = 'charts', label, loadingLabel }: ExportAllButtonProps) {
const [exporting, setExporting] = useState(false);
const handleExport = async () => {
setExporting(true);
try {
await exportAllCharts(containerSelector, zipFilename);
} finally {
setExporting(false);
}
};
return (
<button
className="btn-export-all"
onClick={handleExport}
disabled={exporting}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
{exporting ? loadingLabel : label}
</button>
);
}
export default ExportableChart;