204 lines
5.9 KiB
TypeScript
204 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Slider } from '@/components/ui/slider';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
useLighten,
|
|
useDarken,
|
|
useSaturate,
|
|
useDesaturate,
|
|
useRotate,
|
|
useComplement
|
|
} from '@/lib/color/api/queries';
|
|
import { toast } from 'sonner';
|
|
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
|
|
|
|
interface ManipulationPanelProps {
|
|
color: string;
|
|
onColorChange: (color: string) => void;
|
|
}
|
|
|
|
interface ManipulationRow {
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
value: number;
|
|
setValue: (v: number) => void;
|
|
format: (v: number) => string;
|
|
min: number;
|
|
max: number;
|
|
step: number;
|
|
onApply: () => Promise<void>;
|
|
}
|
|
|
|
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
|
|
const [lightenAmount, setLightenAmount] = useState(0.2);
|
|
const [darkenAmount, setDarkenAmount] = useState(0.2);
|
|
const [saturateAmount, setSaturateAmount] = useState(0.2);
|
|
const [desaturateAmount, setDesaturateAmount] = useState(0.2);
|
|
const [rotateAmount, setRotateAmount] = useState(30);
|
|
|
|
const lightenMutation = useLighten();
|
|
const darkenMutation = useDarken();
|
|
const saturateMutation = useSaturate();
|
|
const desaturateMutation = useDesaturate();
|
|
const rotateMutation = useRotate();
|
|
const complementMutation = useComplement();
|
|
|
|
const isLoading =
|
|
lightenMutation.isPending ||
|
|
darkenMutation.isPending ||
|
|
saturateMutation.isPending ||
|
|
desaturateMutation.isPending ||
|
|
rotateMutation.isPending ||
|
|
complementMutation.isPending;
|
|
|
|
const handleMutation = async (
|
|
mutationFn: (params: any) => Promise<any>,
|
|
params: any,
|
|
successMsg: string,
|
|
errorMsg: string
|
|
) => {
|
|
try {
|
|
const result = await mutationFn(params);
|
|
if (result.colors[0]) {
|
|
onColorChange(result.colors[0].output);
|
|
toast.success(successMsg);
|
|
}
|
|
} catch {
|
|
toast.error(errorMsg);
|
|
}
|
|
};
|
|
|
|
const rows: ManipulationRow[] = [
|
|
{
|
|
label: 'Lighten',
|
|
icon: <Sun className="h-3.5 w-3.5" />,
|
|
value: lightenAmount,
|
|
setValue: setLightenAmount,
|
|
format: (v) => `${(v * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => handleMutation(
|
|
lightenMutation.mutateAsync,
|
|
{ colors: [color], amount: lightenAmount },
|
|
`Lightened by ${(lightenAmount * 100).toFixed(0)}%`,
|
|
'Failed to lighten color'
|
|
),
|
|
},
|
|
{
|
|
label: 'Darken',
|
|
icon: <Moon className="h-3.5 w-3.5" />,
|
|
value: darkenAmount,
|
|
setValue: setDarkenAmount,
|
|
format: (v) => `${(v * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => handleMutation(
|
|
darkenMutation.mutateAsync,
|
|
{ colors: [color], amount: darkenAmount },
|
|
`Darkened by ${(darkenAmount * 100).toFixed(0)}%`,
|
|
'Failed to darken color'
|
|
),
|
|
},
|
|
{
|
|
label: 'Saturate',
|
|
icon: <Droplets className="h-3.5 w-3.5" />,
|
|
value: saturateAmount,
|
|
setValue: setSaturateAmount,
|
|
format: (v) => `${(v * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => handleMutation(
|
|
saturateMutation.mutateAsync,
|
|
{ colors: [color], amount: saturateAmount },
|
|
`Saturated by ${(saturateAmount * 100).toFixed(0)}%`,
|
|
'Failed to saturate color'
|
|
),
|
|
},
|
|
{
|
|
label: 'Desaturate',
|
|
icon: <Droplet className="h-3.5 w-3.5" />,
|
|
value: desaturateAmount,
|
|
setValue: setDesaturateAmount,
|
|
format: (v) => `${(v * 100).toFixed(0)}%`,
|
|
min: 0, max: 1, step: 0.05,
|
|
onApply: () => handleMutation(
|
|
desaturateMutation.mutateAsync,
|
|
{ colors: [color], amount: desaturateAmount },
|
|
`Desaturated by ${(desaturateAmount * 100).toFixed(0)}%`,
|
|
'Failed to desaturate color'
|
|
),
|
|
},
|
|
{
|
|
label: 'Rotate',
|
|
icon: <RotateCcw className="h-3.5 w-3.5" />,
|
|
value: rotateAmount,
|
|
setValue: setRotateAmount,
|
|
format: (v) => `${v}°`,
|
|
min: -180, max: 180, step: 5,
|
|
onApply: () => handleMutation(
|
|
rotateMutation.mutateAsync,
|
|
{ colors: [color], amount: rotateAmount },
|
|
`Rotated hue by ${rotateAmount}°`,
|
|
'Failed to rotate hue'
|
|
),
|
|
},
|
|
];
|
|
|
|
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 {
|
|
toast.error('Failed to generate complement');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{rows.map((row) => (
|
|
<div key={row.label} className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5 text-xs font-medium">
|
|
{row.icon}
|
|
<span>{row.label}</span>
|
|
</div>
|
|
<span className="text-[10px] text-muted-foreground tabular-nums">{row.format(row.value)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Slider
|
|
min={row.min}
|
|
max={row.max}
|
|
step={row.step}
|
|
value={[row.value]}
|
|
onValueChange={(vals) => row.setValue(vals[0])}
|
|
className="flex-1"
|
|
/>
|
|
<Button
|
|
onClick={row.onApply}
|
|
disabled={isLoading}
|
|
variant="outline"
|
|
className="shrink-0 w-16"
|
|
>
|
|
Apply
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<div className="pt-3 border-t">
|
|
<Button
|
|
onClick={handleComplement}
|
|
disabled={isLoading}
|
|
variant="outline"
|
|
className="w-full"
|
|
>
|
|
<ArrowLeftRight className="h-3.5 w-3.5 mr-1.5" />
|
|
Complementary Color
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|