Track when applying our own changes via isApplyingRef to prevent the color update effect from resetting sliders when we trigger the change ourselves. This properly breaks the infinite loop while maintaining reactive behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
238 lines
6.3 KiB
TypeScript
238 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
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) {
|
|
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);
|
|
|
|
const lightenMutation = useLighten();
|
|
const darkenMutation = useDarken();
|
|
const saturateMutation = useSaturate();
|
|
const desaturateMutation = useDesaturate();
|
|
const rotateMutation = useRotate();
|
|
const complementMutation = useComplement();
|
|
|
|
// Track if we're applying our own changes to prevent feedback loop
|
|
const isApplyingRef = useRef(false);
|
|
const baseColorRef = useRef(color);
|
|
|
|
// Update base color only when not applying our own changes
|
|
useEffect(() => {
|
|
if (!isApplyingRef.current) {
|
|
baseColorRef.current = color;
|
|
// Reset sliders when color changes externally
|
|
setLightenAmount(0);
|
|
setDarkenAmount(0);
|
|
setSaturateAmount(0);
|
|
setDesaturateAmount(0);
|
|
setRotateAmount(0);
|
|
}
|
|
}, [color]);
|
|
|
|
// 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Apply darken
|
|
if (darkenAmount > 0) {
|
|
const result = await darkenMutation.mutateAsync({
|
|
colors: [currentColor],
|
|
amount: darkenAmount,
|
|
});
|
|
if (result.colors[0]) {
|
|
currentColor = result.colors[0].output;
|
|
}
|
|
}
|
|
|
|
// Apply saturate
|
|
if (saturateAmount > 0) {
|
|
const result = await saturateMutation.mutateAsync({
|
|
colors: [currentColor],
|
|
amount: saturateAmount,
|
|
});
|
|
if (result.colors[0]) {
|
|
currentColor = result.colors[0].output;
|
|
}
|
|
}
|
|
|
|
// Apply desaturate
|
|
if (desaturateAmount > 0) {
|
|
const result = await desaturateMutation.mutateAsync({
|
|
colors: [currentColor],
|
|
amount: desaturateAmount,
|
|
});
|
|
if (result.colors[0]) {
|
|
currentColor = result.colors[0].output;
|
|
}
|
|
}
|
|
|
|
// Apply rotate
|
|
if (rotateAmount !== 0) {
|
|
const result = await rotateMutation.mutateAsync({
|
|
colors: [currentColor],
|
|
amount: rotateAmount,
|
|
});
|
|
if (result.colors[0]) {
|
|
currentColor = result.colors[0].output;
|
|
}
|
|
}
|
|
|
|
onColorChange(currentColor);
|
|
} catch (error) {
|
|
// Silent error during manipulation
|
|
} finally {
|
|
isApplyingRef.current = false;
|
|
}
|
|
}, 300);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [lightenAmount, darkenAmount, saturateAmount, desaturateAmount, rotateAmount]);
|
|
|
|
const handleComplement = async () => {
|
|
try {
|
|
const result = await complementMutation.mutateAsync([color]);
|
|
if (result.colors[0]) {
|
|
onColorChange(result.colors[0].output);
|
|
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 */}
|
|
<div>
|
|
<Slider
|
|
label="Lighten"
|
|
min={0}
|
|
max={1}
|
|
step={0.05}
|
|
value={lightenAmount}
|
|
onChange={(e) => setLightenAmount(parseFloat(e.target.value))}
|
|
suffix="%"
|
|
showValue
|
|
/>
|
|
</div>
|
|
|
|
{/* Darken */}
|
|
<div>
|
|
<Slider
|
|
label="Darken"
|
|
min={0}
|
|
max={1}
|
|
step={0.05}
|
|
value={darkenAmount}
|
|
onChange={(e) => setDarkenAmount(parseFloat(e.target.value))}
|
|
suffix="%"
|
|
showValue
|
|
/>
|
|
</div>
|
|
|
|
{/* Saturate */}
|
|
<div>
|
|
<Slider
|
|
label="Saturate"
|
|
min={0}
|
|
max={1}
|
|
step={0.05}
|
|
value={saturateAmount}
|
|
onChange={(e) => setSaturateAmount(parseFloat(e.target.value))}
|
|
suffix="%"
|
|
showValue
|
|
/>
|
|
</div>
|
|
|
|
{/* Desaturate */}
|
|
<div>
|
|
<Slider
|
|
label="Desaturate"
|
|
min={0}
|
|
max={1}
|
|
step={0.05}
|
|
value={desaturateAmount}
|
|
onChange={(e) => setDesaturateAmount(parseFloat(e.target.value))}
|
|
suffix="%"
|
|
showValue
|
|
/>
|
|
</div>
|
|
|
|
{/* Rotate Hue */}
|
|
<div>
|
|
<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>
|
|
);
|
|
}
|