saifisvibin's picture
Configure for Hugging Face Spaces deployment with Docker
530365a
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { motion } from 'framer-motion';
import { FaPalette, FaFont, FaArrowsAlt, FaChevronLeft, FaChevronRight, FaMousePointer } from 'react-icons/fa';
const DesignStep = ({ fileData, designParams, setDesignParams, mappings, setMappings, onNext, onBack }) => {
const [previewImage, setPreviewImage] = useState(null);
const [loadingPreview, setLoadingPreview] = useState(false);
const [previewName, setPreviewName] = useState("Sample Name");
const [pdfSize, setPdfSize] = useState({ width: 595, height: 842 });
const imageRef = useRef(null);
// Fetch clean preview once on mount
useEffect(() => {
fetchCleanPreview();
}, []);
const fetchCleanPreview = async () => {
setLoadingPreview(true);
const formData = new FormData();
formData.append("name", " "); // Space for clean background (avoids empty string issues)
formData.append("name_color", "#000000");
formData.append("font_size", "60");
formData.append("fontname", "helv");
try {
const response = await axios.post("/api/certificates/preview", formData, {
responseType: 'blob'
});
const imageUrl = URL.createObjectURL(response.data);
setPreviewImage(imageUrl);
} catch (error) {
console.error("Error fetching preview:", error);
if (error.response) {
console.error("Response data:", error.response.data);
console.error("Response status:", error.response.status);
}
} finally {
setLoadingPreview(false);
}
};
const handleImageLoad = (e) => {
const img = e.target;
setPdfSize({ width: img.naturalWidth, height: img.naturalHeight });
};
const handleImageClick = (e) => {
if (!imageRef.current) return;
const rect = imageRef.current.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const scaleX = pdfSize.width / rect.width;
const scaleY = pdfSize.height / rect.height;
const pdfX = clickX * scaleX;
const pdfY = clickY * scaleY;
setDesignParams({ ...designParams, x: Math.round(pdfX), y: Math.round(pdfY) });
};
const handleKeyboardMove = (e) => {
if (!designParams.x && !designParams.y) return;
const stepX = e.shiftKey ? 60 : 30;
const stepY = e.shiftKey ? 40 : 20;
let newX = designParams.x || pdfSize.width / 2;
let newY = designParams.y || pdfSize.height / 2;
switch (e.key) {
case 'ArrowLeft': newX -= stepX; break;
case 'ArrowRight': newX += stepX; break;
case 'ArrowUp': newY -= stepY; break;
case 'ArrowDown': newY += stepY; break;
default: return;
}
e.preventDefault();
setDesignParams({ ...designParams, x: Math.round(newX), y: Math.round(newY) });
};
const InputGroup = ({ label, icon: Icon, children }) => (
<div className="bg-white border border-gray-200 p-5 rounded-lg shadow-sm">
<div className="flex items-center space-x-2 mb-4 text-blue-600">
<Icon className="text-lg" />
<span className="font-semibold text-sm uppercase tracking-wide">{label}</span>
</div>
{children}
</div>
);
return (
<div className="flex flex-col lg:flex-row gap-8">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="w-full lg:w-1/3 space-y-6"
>
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900">Customize Design</h2>
<p className="text-gray-600 text-sm mt-1">Fine-tune the appearance of your certificates.</p>
</div>
<InputGroup label="Data Mapping" icon={FaFont}>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Name Column</label>
<select
className="w-full border border-gray-300 rounded-lg p-2.5 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none bg-white"
value={mappings.name_column}
onChange={(e) => setMappings({ ...mappings, name_column: e.target.value })}
>
<option value="">Select Column</option>
{fileData.columns.map(col => <option key={col} value={col}>{col}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Email Column</label>
<select
className="w-full border border-gray-300 rounded-lg p-2.5 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none bg-white"
value={mappings.email_column}
onChange={(e) => setMappings({ ...mappings, email_column: e.target.value })}
>
<option value="">Select Column</option>
{fileData.columns.map(col => <option key={col} value={col}>{col}</option>)}
</select>
</div>
</div>
</InputGroup>
<InputGroup label="Typography & Color" icon={FaPalette}>
<div className="space-y-4">
<div>
<div className="flex justify-between text-xs font-medium text-gray-700 mb-1">
<span>Font Size</span>
<span className="text-blue-600">{designParams.font_size}px</span>
</div>
<input
type="range" min="10" max="200"
value={designParams.font_size}
onChange={(e) => setDesignParams({ ...designParams, font_size: parseInt(e.target.value) })}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Text Color</label>
<div className="flex items-center space-x-3">
<input
type="color"
value={designParams.name_color}
onChange={(e) => setDesignParams({ ...designParams, name_color: e.target.value })}
className="w-10 h-10 rounded-lg cursor-pointer border border-gray-300"
/>
<span className="text-gray-700 font-mono text-sm">{designParams.name_color}</span>
</div>
</div>
</div>
</InputGroup>
<InputGroup label="Positioning" icon={FaArrowsAlt}>
<div className="space-y-3">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start space-x-2">
<FaMousePointer className="text-blue-600 mt-0.5 flex-shrink-0" />
<div className="text-xs text-blue-700">
<p className="font-semibold mb-1">Real-Time Positioning</p>
<ul className="space-y-1">
<li>• Click preview to set position</li>
<li>• Arrow ←→: 30px, ↑↓: 20px (Shift: 2x)</li>
<li>• Drag sliders for smooth control</li>
</ul>
</div>
</div>
</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-xs font-medium text-gray-700 mb-1">
<label>X Position</label>
<span className="text-blue-600">{designParams.x || 'Auto'}</span>
</div>
<input
type="range"
min="0"
max={pdfSize.width}
value={designParams.x || pdfSize.width / 2}
onChange={(e) => setDesignParams({ ...designParams, x: parseInt(e.target.value) })}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
<div>
<div className="flex justify-between text-xs font-medium text-gray-700 mb-1">
<label>Y Position</label>
<span className="text-blue-600">{designParams.y || 'Auto'}</span>
</div>
<input
type="range"
min="0"
max={pdfSize.height}
value={designParams.y || pdfSize.height / 2}
onChange={(e) => setDesignParams({ ...designParams, y: parseInt(e.target.value) })}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
</div>
<button
onClick={() => setDesignParams({ ...designParams, x: null, y: null })}
className="w-full text-xs text-blue-600 hover:text-blue-700 font-medium mt-2"
>
Reset to Auto Center
</button>
</div>
</InputGroup>
<div className="flex space-x-3 pt-4">
<button
onClick={onBack}
className="flex-1 py-3 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors flex items-center justify-center space-x-2 font-medium"
>
<FaChevronLeft className="text-xs" />
<span>Back</span>
</button>
<button
onClick={onNext}
disabled={!mappings.name_column || !mappings.email_column}
className={`flex-1 py-3 rounded-lg text-white font-semibold shadow-md flex items-center justify-center space-x-2 transition-all ${!mappings.name_column || !mappings.email_column
? 'bg-gray-300 cursor-not-allowed text-gray-500'
: 'bg-blue-600 hover:bg-blue-700 hover:shadow-lg'
}`}
>
<span>Next Step</span>
<FaChevronRight className="text-xs" />
</button>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full lg:w-2/3 bg-gray-50 rounded-lg border border-gray-200 p-6 flex items-center justify-center min-h-[600px]"
tabIndex={0}
onKeyDown={handleKeyboardMove}
>
{loadingPreview ? (
<div className="flex flex-col items-center space-y-4">
<div className="w-12 h-12 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
<p className="text-gray-600 font-medium">Loading Preview...</p>
</div>
) : previewImage ? (
<div className="relative">
<img
ref={imageRef}
src={previewImage}
alt="Certificate Preview"
className="max-w-full max-h-full shadow-xl rounded border border-gray-300 cursor-crosshair"
onClick={handleImageClick}
onLoad={handleImageLoad}
/>
{/* Real-time Frontend Text Overlay */}
{designParams.x !== null && designParams.y !== null && imageRef.current && (
<div
className="absolute pointer-events-none flex items-center justify-center"
style={{
left: `${(designParams.x / pdfSize.width) * 100}%`,
top: `${(designParams.y / pdfSize.height) * 100}%`,
transform: 'translate(-50%, -50%)',
fontSize: `${(designParams.font_size / pdfSize.width) * imageRef.current.offsetWidth}px`,
color: designParams.name_color,
fontFamily: 'Helvetica, Arial, sans-serif', // Approximate PDF font
fontWeight: 'bold',
whiteSpace: 'nowrap',
lineHeight: 1,
// No transition for instant responsiveness
}}
>
{previewName}
</div>
)}
{designParams.x !== null && designParams.y !== null && (
<div className="absolute top-2 right-2 bg-black/70 text-white text-xs px-3 py-1 rounded">
Position: {Math.round(designParams.x)}, {Math.round(designParams.y)}
</div>
)}
</div>
) : (
<div className="text-gray-400 text-center">
<p className="text-lg font-medium">Preview Area</p>
<p className="text-sm">Upload files to see a preview</p>
</div>
)}
</motion.div>
</div>
);
};
export default DesignStep;