feat: Add PDF download button for car images in admin car detail modal
- Download all car images as a single PDF (A4 landscape, one image per page) - Button shows image count and displays in modal header for easy access - Uses jsPDF library for client-side PDF generation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import Image from 'next/image';
|
||||
import { Reorder, useDragControls } from 'framer-motion';
|
||||
import api, { heroBannersApi, carmodooApi, vehicleRequestsApi, carsApi } from '@/lib/api';
|
||||
import { translateCarName } from '@/lib/i18n';
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
interface CarmodooMaker {
|
||||
code: string;
|
||||
@@ -1258,6 +1259,73 @@ export default function CarsAdminPage() {
|
||||
setEditingComment(false);
|
||||
};
|
||||
|
||||
const [pdfDownloading, setPdfDownloading] = useState(false);
|
||||
|
||||
const handleDownloadImagesPdf = async () => {
|
||||
if (!selectedCar?.images || selectedCar.images.length === 0) return;
|
||||
|
||||
setPdfDownloading(true);
|
||||
try {
|
||||
const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
|
||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||
|
||||
const images = selectedCar.images;
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
if (i > 0) pdf.addPage();
|
||||
|
||||
const imgUrl = getImageUrl(images[i].url);
|
||||
|
||||
try {
|
||||
const response = await fetch(imgUrl);
|
||||
const blob = await response.blob();
|
||||
const dataUrl = await new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
const img = new window.Image();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(new Error(`Failed to load image ${i + 1}`));
|
||||
img.src = dataUrl;
|
||||
});
|
||||
|
||||
const imgRatio = img.width / img.height;
|
||||
const pageRatio = pageWidth / pageHeight;
|
||||
|
||||
let drawWidth: number, drawHeight: number, x: number, y: number;
|
||||
if (imgRatio > pageRatio) {
|
||||
drawWidth = pageWidth;
|
||||
drawHeight = pageWidth / imgRatio;
|
||||
x = 0;
|
||||
y = (pageHeight - drawHeight) / 2;
|
||||
} else {
|
||||
drawHeight = pageHeight;
|
||||
drawWidth = pageHeight * imgRatio;
|
||||
x = (pageWidth - drawWidth) / 2;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
pdf.addImage(dataUrl, 'JPEG', x, y, drawWidth, drawHeight);
|
||||
} catch (imgError) {
|
||||
console.error(`Failed to load image ${i + 1}:`, imgError);
|
||||
pdf.setFontSize(14);
|
||||
pdf.text(`Image ${i + 1} - Failed to load`, pageWidth / 2, pageHeight / 2, { align: 'center' });
|
||||
}
|
||||
}
|
||||
|
||||
const carName = selectedCar.car_name.replace(/[^a-zA-Z0-9가-힣\s]/g, '').trim();
|
||||
pdf.save(`${carName}_images.pdf`);
|
||||
} catch (error) {
|
||||
console.error('PDF generation failed:', error);
|
||||
alert('PDF generation failed. Please try again.');
|
||||
} finally {
|
||||
setPdfDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditComment = () => {
|
||||
if (dealerComment) {
|
||||
setEditCommentData({
|
||||
@@ -2667,16 +2735,39 @@ export default function CarsAdminPage() {
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Modal Header */}
|
||||
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center">
|
||||
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center z-10">
|
||||
<h2 className="text-xl font-bold text-gray-800">{selectedCar.car_name}</h2>
|
||||
<button
|
||||
onClick={closeDetailModal}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
{selectedCar.images && selectedCar.images.length > 0 && (
|
||||
<button
|
||||
onClick={handleDownloadImagesPdf}
|
||||
disabled={pdfDownloading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 transition font-medium text-sm"
|
||||
>
|
||||
{pdfDownloading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Generating...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span>PDF ({selectedCar.images.length})</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={closeDetailModal}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Content */}
|
||||
|
||||
Reference in New Issue
Block a user