2025-11-07 11:28:35 +01:00
|
|
|
'use client';
|
|
|
|
|
|
2025-11-17 22:13:49 +01:00
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
2025-11-07 11:28:35 +01:00
|
|
|
import { Slider } from '@/components/ui/slider';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import {
|
|
|
|
|
useLighten,
|
|
|
|
|
useDarken,
|
|
|
|
|
useSaturate,
|
|
|
|
|
useDesaturate,
|
|
|
|
|
useRotate,
|
|
|
|
|
useComplement
|
|
|
|
|
} from '@/lib/api/queries';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
|
|
|
|
interface ManipulationPanelProps {
|
|
|
|
|
color: string;
|
|
|
|
|
onColorChange: (color: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
|
2025-11-17 22:13:49 +01:00
|
|
|
const [lightenAmount, setLightenAmount] = useState(0);
|
|
|
|
|
const [darkenAmount, setDarkenAmount] = useState(0);
|
|
|
|
|
const [saturateAmount, setSaturateAmount] = useState(0);
|
|
|
|
|
const [desaturateAmount, setDesaturateAmount] = useState(0);
|
|
|
|
|
const [rotateAmount, setRotateAmount] = useState(0);
|
2025-11-07 11:28:35 +01:00
|
|
|
|
|
|
|
|
const lightenMutation = useLighten();
|
|
|
|
|
const darkenMutation = useDarken();
|
|
|
|
|
const saturateMutation = useSaturate();
|
|
|
|
|
const desaturateMutation = useDesaturate();
|
|
|
|
|
const rotateMutation = useRotate();
|
|
|
|
|
const complementMutation = useComplement();
|
|
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
// Track if we're applying our own changes to prevent feedback loop
|
|
|
|
|
const isApplyingRef = useRef(false);
|
|
|
|
|
const baseColorRef = useRef(color);
|
2025-11-17 22:02:57 +01:00
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
// Update base color only when not applying our own changes
|
2025-11-17 22:02:57 +01:00
|
|
|
useEffect(() => {
|
2025-11-17 22:19:51 +01:00
|
|
|
if (!isApplyingRef.current) {
|
|
|
|
|
baseColorRef.current = color;
|
|
|
|
|
// Reset sliders when color changes externally
|
|
|
|
|
setLightenAmount(0);
|
|
|
|
|
setDarkenAmount(0);
|
|
|
|
|
setSaturateAmount(0);
|
|
|
|
|
setDesaturateAmount(0);
|
|
|
|
|
setRotateAmount(0);
|
|
|
|
|
}
|
2025-11-17 22:13:49 +01:00
|
|
|
}, [color]);
|
|
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
// Debounced effect to apply manipulations
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const timer = setTimeout(async () => {
|
|
|
|
|
// Skip if all sliders are at neutral position
|
|
|
|
|
if (lightenAmount === 0 && darkenAmount === 0 && saturateAmount === 0 &&
|
|
|
|
|
desaturateAmount === 0 && rotateAmount === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-17 22:13:49 +01:00
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
isApplyingRef.current = true;
|
|
|
|
|
let currentColor = baseColorRef.current;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Apply lighten
|
|
|
|
|
if (lightenAmount > 0) {
|
|
|
|
|
const result = await lightenMutation.mutateAsync({
|
|
|
|
|
colors: [currentColor],
|
|
|
|
|
amount: lightenAmount,
|
|
|
|
|
});
|
|
|
|
|
if (result.colors[0]) {
|
|
|
|
|
currentColor = result.colors[0].output;
|
|
|
|
|
}
|
2025-11-17 22:02:57 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
// Apply darken
|
|
|
|
|
if (darkenAmount > 0) {
|
|
|
|
|
const result = await darkenMutation.mutateAsync({
|
|
|
|
|
colors: [currentColor],
|
|
|
|
|
amount: darkenAmount,
|
|
|
|
|
});
|
|
|
|
|
if (result.colors[0]) {
|
|
|
|
|
currentColor = result.colors[0].output;
|
|
|
|
|
}
|
2025-11-17 22:02:57 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
// Apply saturate
|
|
|
|
|
if (saturateAmount > 0) {
|
|
|
|
|
const result = await saturateMutation.mutateAsync({
|
|
|
|
|
colors: [currentColor],
|
|
|
|
|
amount: saturateAmount,
|
|
|
|
|
});
|
|
|
|
|
if (result.colors[0]) {
|
|
|
|
|
currentColor = result.colors[0].output;
|
|
|
|
|
}
|
2025-11-17 22:02:57 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
// Apply desaturate
|
|
|
|
|
if (desaturateAmount > 0) {
|
|
|
|
|
const result = await desaturateMutation.mutateAsync({
|
|
|
|
|
colors: [currentColor],
|
|
|
|
|
amount: desaturateAmount,
|
|
|
|
|
});
|
|
|
|
|
if (result.colors[0]) {
|
|
|
|
|
currentColor = result.colors[0].output;
|
|
|
|
|
}
|
2025-11-17 22:02:57 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
// Apply rotate
|
|
|
|
|
if (rotateAmount !== 0) {
|
|
|
|
|
const result = await rotateMutation.mutateAsync({
|
|
|
|
|
colors: [currentColor],
|
|
|
|
|
amount: rotateAmount,
|
|
|
|
|
});
|
|
|
|
|
if (result.colors[0]) {
|
|
|
|
|
currentColor = result.colors[0].output;
|
|
|
|
|
}
|
2025-11-17 22:02:57 +01:00
|
|
|
}
|
2025-11-17 22:13:49 +01:00
|
|
|
|
|
|
|
|
onColorChange(currentColor);
|
2025-11-17 22:19:51 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
// Silent error during manipulation
|
|
|
|
|
} finally {
|
|
|
|
|
isApplyingRef.current = false;
|
2025-11-17 22:13:49 +01:00
|
|
|
}
|
2025-11-17 22:02:57 +01:00
|
|
|
}, 300);
|
2025-11-17 22:13:49 +01:00
|
|
|
|
2025-11-17 22:19:51 +01:00
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
}, [lightenAmount, darkenAmount, saturateAmount, desaturateAmount, rotateAmount]);
|
2025-11-07 11:28:35 +01:00
|
|
|
|
|
|
|
|
const handleComplement = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await complementMutation.mutateAsync([color]);
|
2025-11-07 14:33:38 +01:00
|
|
|
if (result.colors[0]) {
|
|
|
|
|
onColorChange(result.colors[0].output);
|
2025-11-07 11:28:35 +01:00
|
|
|
toast.success('Generated complementary color');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error('Failed to generate complement');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isLoading =
|
|
|
|
|
lightenMutation.isPending ||
|
|
|
|
|
darkenMutation.isPending ||
|
|
|
|
|
saturateMutation.isPending ||
|
|
|
|
|
desaturateMutation.isPending ||
|
|
|
|
|
rotateMutation.isPending ||
|
|
|
|
|
complementMutation.isPending;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Lighten */}
|
2025-11-17 22:02:57 +01:00
|
|
|
<div>
|
2025-11-07 11:28:35 +01:00
|
|
|
<Slider
|
|
|
|
|
label="Lighten"
|
|
|
|
|
min={0}
|
|
|
|
|
max={1}
|
|
|
|
|
step={0.05}
|
|
|
|
|
value={lightenAmount}
|
|
|
|
|
onChange={(e) => setLightenAmount(parseFloat(e.target.value))}
|
|
|
|
|
suffix="%"
|
|
|
|
|
showValue
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Darken */}
|
2025-11-17 22:02:57 +01:00
|
|
|
<div>
|
2025-11-07 11:28:35 +01:00
|
|
|
<Slider
|
|
|
|
|
label="Darken"
|
|
|
|
|
min={0}
|
|
|
|
|
max={1}
|
|
|
|
|
step={0.05}
|
|
|
|
|
value={darkenAmount}
|
|
|
|
|
onChange={(e) => setDarkenAmount(parseFloat(e.target.value))}
|
|
|
|
|
suffix="%"
|
|
|
|
|
showValue
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Saturate */}
|
2025-11-17 22:02:57 +01:00
|
|
|
<div>
|
2025-11-07 11:28:35 +01:00
|
|
|
<Slider
|
|
|
|
|
label="Saturate"
|
|
|
|
|
min={0}
|
|
|
|
|
max={1}
|
|
|
|
|
step={0.05}
|
|
|
|
|
value={saturateAmount}
|
|
|
|
|
onChange={(e) => setSaturateAmount(parseFloat(e.target.value))}
|
|
|
|
|
suffix="%"
|
|
|
|
|
showValue
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Desaturate */}
|
2025-11-17 22:02:57 +01:00
|
|
|
<div>
|
2025-11-07 11:28:35 +01:00
|
|
|
<Slider
|
|
|
|
|
label="Desaturate"
|
|
|
|
|
min={0}
|
|
|
|
|
max={1}
|
|
|
|
|
step={0.05}
|
|
|
|
|
value={desaturateAmount}
|
|
|
|
|
onChange={(e) => setDesaturateAmount(parseFloat(e.target.value))}
|
|
|
|
|
suffix="%"
|
|
|
|
|
showValue
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Rotate Hue */}
|
2025-11-17 22:02:57 +01:00
|
|
|
<div>
|
2025-11-07 11:28:35 +01:00
|
|
|
<Slider
|
|
|
|
|
label="Rotate Hue"
|
|
|
|
|
min={-180}
|
|
|
|
|
max={180}
|
|
|
|
|
step={5}
|
|
|
|
|
value={rotateAmount}
|
|
|
|
|
onChange={(e) => setRotateAmount(parseInt(e.target.value))}
|
|
|
|
|
suffix="°"
|
|
|
|
|
showValue
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Quick Actions */}
|
|
|
|
|
<div className="pt-4 border-t space-y-2">
|
|
|
|
|
<h3 className="text-sm font-medium mb-3">Quick Actions</h3>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleComplement}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
|
|
|
|
Get Complementary Color
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|