feat(qr): Add Gradients, Inner Eye styling, and Logo Presets to Visual Customization Studio

This commit is contained in:
Mohan Ki
2026-07-27 19:26:51 +05:30
parent bd17947106
commit 2bfa741a71
7 changed files with 162 additions and 49 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -286,7 +286,7 @@ export default function DashboardPage() {
<div className="flex gap-2">
<Link href={`/dashboard/edit/${qr.id}`} className="flex-1 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-semibold py-2 px-3 rounded-lg transition-colors text-sm text-center">
Edit Link
Edit QR Code
</Link>
{/* Move Folder Dropdown Toggle */}
+161 -48
View File
@@ -9,13 +9,34 @@ interface DesignPanelProps {
export default function DesignPanel({ options, onChange }: DesignPanelProps) {
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const [dotColorMode, setDotColorMode] = React.useState<'solid' | 'gradient'>(
options.dotsOptions?.gradient ? 'gradient' : 'solid'
);
const handleDotColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newColor = e.target.value;
onChange({
...options,
dotsOptions: { ...options.dotsOptions, color: newColor },
cornersSquareOptions: { ...options.cornersSquareOptions, color: newColor },
cornersDotOptions: { ...options.cornersDotOptions, color: newColor }
dotsOptions: { ...options.dotsOptions, color: newColor, gradient: undefined },
cornersSquareOptions: { ...options.cornersSquareOptions, color: newColor, gradient: undefined },
cornersDotOptions: { ...options.cornersDotOptions, color: newColor, gradient: undefined }
});
};
const handleDotGradientChange = (color1: string, color2: string) => {
const gradient: any = {
type: 'linear',
rotation: Math.PI / 4,
colorStops: [
{ offset: 0, color: color1 },
{ offset: 1, color: color2 }
]
};
onChange({
...options,
dotsOptions: { ...options.dotsOptions, gradient, color: undefined },
cornersSquareOptions: { ...options.cornersSquareOptions, gradient, color: undefined },
cornersDotOptions: { ...options.cornersDotOptions, gradient, color: undefined }
});
};
@@ -33,6 +54,13 @@ export default function DesignPanel({ options, onChange }: DesignPanelProps) {
});
};
const handleInnerEyeTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onChange({
...options,
cornersDotOptions: { ...options.cornersDotOptions, type: e.target.value as any }
});
};
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
@@ -48,6 +76,13 @@ export default function DesignPanel({ options, onChange }: DesignPanelProps) {
reader.readAsDataURL(file);
};
const setPresetLogo = (path: string) => {
onChange({
...options,
image: path
});
};
const removeLogo = () => {
onChange({
...options,
@@ -55,75 +90,153 @@ export default function DesignPanel({ options, onChange }: DesignPanelProps) {
});
};
// Helper to extract colors from gradient
const getGradientColors = () => {
if (options.dotsOptions?.gradient?.colorStops && options.dotsOptions.gradient.colorStops.length >= 2) {
return [
options.dotsOptions.gradient.colorStops[0].color,
options.dotsOptions.gradient.colorStops[1].color
];
}
return ['#4f46e5', '#ec4899'];
};
const [c1, c2] = getGradientColors();
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
<h2 className="text-xl font-bold text-gray-800 border-b pb-4">Design Customization</h2>
{/* Colors */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Primary Color (Dots & Eyes)</label>
<div className="flex items-center gap-4">
<input
type="color"
value={options.dotsOptions?.color || '#000000'}
onChange={handleColorChange}
className="w-10 h-10 rounded cursor-pointer border-0 p-0"
/>
<span className="text-gray-500 font-mono text-sm">{options.dotsOptions?.color}</span>
<label className="block text-sm font-medium text-gray-700 mb-2">QR Code Color</label>
<div className="flex gap-2 mb-3">
<button
type="button"
onClick={() => setDotColorMode('solid')}
className={`px-3 py-1 text-sm rounded-lg font-medium transition-colors ${dotColorMode === 'solid' ? 'bg-indigo-100 text-indigo-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
>
Solid Color
</button>
<button
type="button"
onClick={() => setDotColorMode('gradient')}
className={`px-3 py-1 text-sm rounded-lg font-medium transition-colors ${dotColorMode === 'gradient' ? 'bg-indigo-100 text-indigo-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
>
Gradient
</button>
</div>
{dotColorMode === 'solid' ? (
<div className="flex items-center gap-4">
<input
type="color"
value={options.dotsOptions?.color || '#000000'}
onChange={handleDotColorChange}
className="w-10 h-10 rounded cursor-pointer border-0 p-0"
/>
<span className="text-gray-500 font-mono text-sm">{options.dotsOptions?.color || '#000000'}</span>
</div>
) : (
<div className="flex items-center gap-4 bg-gray-50 p-3 rounded-lg border border-gray-200">
<div className="flex flex-col gap-1">
<span className="text-xs text-gray-500 font-medium uppercase">Color 1</span>
<input type="color" value={c1} onChange={(e) => handleDotGradientChange(e.target.value, c2)} className="w-8 h-8 rounded cursor-pointer" />
</div>
<svg className="w-5 h-5 text-gray-400 mt-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" /></svg>
<div className="flex flex-col gap-1">
<span className="text-xs text-gray-500 font-medium uppercase">Color 2</span>
<input type="color" value={c2} onChange={(e) => handleDotGradientChange(c1, e.target.value)} className="w-8 h-8 rounded cursor-pointer" />
</div>
</div>
)}
</div>
{/* Shapes */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Data Pattern</label>
<select
value={options.dotsOptions?.type || 'square'}
onChange={handleDotTypeChange}
className="w-full border-gray-300 rounded-lg shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white text-sm"
>
<option value="square">Square</option>
<option value="dots">Dots</option>
<option value="rounded">Rounded</option>
<option value="extra-rounded">Extra Rounded</option>
<option value="classy">Classy</option>
<option value="classy-rounded">Classy Rounded</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Outer Eye Frame</label>
<select
value={options.cornersSquareOptions?.type || 'square'}
onChange={handleEyeTypeChange}
className="w-full border-gray-300 rounded-lg shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white text-sm"
>
<option value="square">Square</option>
<option value="dot">Dot</option>
<option value="extra-rounded">Extra Rounded</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Inner Eye Dot</label>
<select
value={options.cornersDotOptions?.type || 'square'}
onChange={handleInnerEyeTypeChange}
className="w-full border-gray-300 rounded-lg shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white text-sm"
>
<option value="square">Square</option>
<option value="dot">Dot</option>
</select>
</div>
</div>
{/* Logo */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Logo</label>
<div className="border-t pt-4">
<label className="block text-sm font-medium text-gray-700 mb-3">Center Logo</label>
<div className="flex gap-2 mb-4">
<button type="button" onClick={() => setPresetLogo('/logos/facebook.svg')} className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-indigo-300 transition-colors bg-white">
<img src="/logos/facebook.svg" alt="Facebook" className="w-6 h-6" />
</button>
<button type="button" onClick={() => setPresetLogo('/logos/instagram.svg')} className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-indigo-300 transition-colors bg-white">
<img src="/logos/instagram.svg" alt="Instagram" className="w-6 h-6" />
</button>
<button type="button" onClick={() => setPresetLogo('/logos/x.svg')} className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-indigo-300 transition-colors bg-white">
<img src="/logos/x.svg" alt="X" className="w-6 h-6" />
</button>
<button type="button" onClick={() => setPresetLogo('/logos/linkedin.svg')} className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-indigo-300 transition-colors bg-white">
<img src="/logos/linkedin.svg" alt="LinkedIn" className="w-6 h-6" />
</button>
<button type="button" onClick={() => setPresetLogo('/logos/youtube.svg')} className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-indigo-300 transition-colors bg-white">
<img src="/logos/youtube.svg" alt="YouTube" className="w-6 h-6" />
</button>
</div>
<div className="flex items-center gap-4">
<input
type="file"
accept="image/*"
onChange={handleLogoUpload}
className="text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
className="text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 cursor-pointer"
/>
{options.image && (
<button
type="button"
onClick={removeLogo}
className="text-sm text-red-600 hover:text-red-700 font-medium"
className="text-sm text-red-600 hover:text-red-700 font-bold bg-red-50 px-3 py-1.5 rounded-lg transition-colors border border-red-100"
>
Remove Logo
Clear Logo
</button>
)}
</div>
</div>
{/* Shapes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Dot Pattern</label>
<select
value={options.dotsOptions?.type || 'square'}
onChange={handleDotTypeChange}
className="w-full border-gray-300 rounded-lg shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white"
>
<option value="square">Square</option>
<option value="dots">Dots</option>
<option value="rounded">Rounded</option>
<option value="extra-rounded">Extra Rounded</option>
<option value="classy">Classy</option>
<option value="classy-rounded">Classy Rounded</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Eye Frame Shape</label>
<select
value={options.cornersSquareOptions?.type || 'square'}
onChange={handleEyeTypeChange}
className="w-full border-gray-300 rounded-lg shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white"
>
<option value="square">Square</option>
<option value="dot">Dot</option>
<option value="extra-rounded">Extra Rounded</option>
</select>
</div>
</div>
);
}