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">
|
<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">
|
<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>
|
</Link>
|
||||||
|
|
||||||
{/* Move Folder Dropdown Toggle */}
|
{/* Move Folder Dropdown Toggle */}
|
||||||
|
|||||||
@@ -9,13 +9,34 @@ interface DesignPanelProps {
|
|||||||
|
|
||||||
export default function DesignPanel({ options, onChange }: 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;
|
const newColor = e.target.value;
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
dotsOptions: { ...options.dotsOptions, color: newColor },
|
dotsOptions: { ...options.dotsOptions, color: newColor, gradient: undefined },
|
||||||
cornersSquareOptions: { ...options.cornersSquareOptions, color: newColor },
|
cornersSquareOptions: { ...options.cornersSquareOptions, color: newColor, gradient: undefined },
|
||||||
cornersDotOptions: { ...options.cornersDotOptions, color: newColor }
|
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 }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +53,13 @@ export default function DesignPanel({ options, onChange }: DesignPanelProps) {
|
|||||||
cornersSquareOptions: { ...options.cornersSquareOptions, type: e.target.value as any }
|
cornersSquareOptions: { ...options.cornersSquareOptions, type: e.target.value as any }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInnerEyeTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
cornersDotOptions: { ...options.cornersDotOptions, type: e.target.value as any }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -48,6 +76,13 @@ export default function DesignPanel({ options, onChange }: DesignPanelProps) {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setPresetLogo = (path: string) => {
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
image: path
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const removeLogo = () => {
|
const removeLogo = () => {
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
@@ -55,74 +90,152 @@ 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 (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
<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>
|
<h2 className="text-xl font-bold text-gray-800 border-b pb-4">Design Customization</h2>
|
||||||
|
|
||||||
{/* Colors */}
|
{/* Colors */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Primary Color (Dots & Eyes)</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">QR Code Color</label>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<input
|
<div className="flex gap-2 mb-3">
|
||||||
type="color"
|
<button
|
||||||
value={options.dotsOptions?.color || '#000000'}
|
type="button"
|
||||||
onChange={handleColorChange}
|
onClick={() => setDotColorMode('solid')}
|
||||||
className="w-10 h-10 rounded cursor-pointer border-0 p-0"
|
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'}`}
|
||||||
/>
|
>
|
||||||
<span className="text-gray-500 font-mono text-sm">{options.dotsOptions?.color}</span>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div>
|
<div className="border-t pt-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Logo</label>
|
<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">
|
<div className="flex items-center gap-4">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={handleLogoUpload}
|
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 && (
|
{options.image && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={removeLogo}
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user