feat(qr): Add Gradients, Inner Eye styling, and Logo Presets to Visual Customization Studio
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user