Files
pastel-ui/app/batch/page.tsx
valknarness 93889ab9bd fix: correct API integration and complete missing features
Fix API response format mismatches and implement all remaining features:

**API Integration Fixes:**
- Fix ManipulationPanel to use `colors` instead of `results` from API responses
- Fix gradient endpoint to use `gradient` array from API response
- Fix color blindness simulator to use correct field names (`input`/`output` vs `original`/`simulated`)
- Fix text color optimizer request field (`backgrounds` vs `background_colors`)
- Fix method name casing: `simulateColorBlindness` (capital B)
- Add palette generation endpoint integration

**Type Definition Updates:**
- Update GradientData to match API structure with `gradient` array
- Update ColorBlindnessData to use `colors` with `input`/`output`/`difference_percentage`
- Update TextColorData to use `colors` with `textcolor`/`wcag_aa`/`wcag_aaa` fields
- Add PaletteGenerateRequest and PaletteGenerateData types

**Completed Features:**
- Harmony Palettes: Now uses dedicated `/palettes/generate` API endpoint
  - Simplified from 80 lines of manual color theory to single API call
  - Supports 6 harmony types: monochromatic, analogous, complementary, split-complementary, triadic, tetradic
- Text Color Optimizer: Full implementation with WCAG compliance checking
  - Automatic black/white text color selection
  - Live preview with contrast ratios
  - AA/AAA compliance indicators
- Color Blindness Simulator: Fixed and working
  - Shows difference percentage for each simulation
  - Side-by-side comparison view
- Gradient Creator: Fixed to use correct API response structure
- Batch Operations: Fixed to extract output colors correctly

**UI Improvements:**
- Enable all accessibility tool cards (remove "Coming Soon" badges)
- Enable harmony palettes card
- Add safety check for gradient state to prevent undefined errors

All features now fully functional and properly integrated with Pastel API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 14:33:38 +01:00

194 lines
6.7 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { PaletteGrid } from '@/components/color/PaletteGrid';
import { ExportMenu } from '@/components/tools/ExportMenu';
import { useLighten, useDarken, useSaturate, useDesaturate, useRotate } from '@/lib/api/queries';
import { Loader2, Upload, Download } from 'lucide-react';
import { toast } from 'sonner';
type Operation = 'lighten' | 'darken' | 'saturate' | 'desaturate' | 'rotate';
export default function BatchPage() {
const [inputColors, setInputColors] = useState('');
const [operation, setOperation] = useState<Operation>('lighten');
const [amount, setAmount] = useState(0.2);
const [outputColors, setOutputColors] = useState<string[]>([]);
const lightenMutation = useLighten();
const darkenMutation = useDarken();
const saturateMutation = useSaturate();
const desaturateMutation = useDesaturate();
const rotateMutation = useRotate();
const parseColors = (text: string): string[] => {
// Parse colors from text (one per line, or comma-separated)
return text
.split(/[\n,]/)
.map((c) => c.trim())
.filter((c) => c.length > 0 && c.match(/^#?[0-9a-fA-F]{3,8}$/));
};
const handleProcess = async () => {
const colors = parseColors(inputColors);
if (colors.length === 0) {
toast.error('No valid colors found');
return;
}
if (colors.length > 100) {
toast.error('Maximum 100 colors allowed');
return;
}
try {
let result;
switch (operation) {
case 'lighten':
result = await lightenMutation.mutateAsync({ colors, amount });
break;
case 'darken':
result = await darkenMutation.mutateAsync({ colors, amount });
break;
case 'saturate':
result = await saturateMutation.mutateAsync({ colors, amount });
break;
case 'desaturate':
result = await desaturateMutation.mutateAsync({ colors, amount });
break;
case 'rotate':
result = await rotateMutation.mutateAsync({ colors, amount: amount * 360 });
break;
}
// Extract output colors from the result
const processed = result.colors.map((c) => c.output);
setOutputColors(processed);
toast.success(`Processed ${processed.length} colors`);
} catch (error) {
toast.error('Failed to process colors');
console.error(error);
}
};
const isPending =
lightenMutation.isPending ||
darkenMutation.isPending ||
saturateMutation.isPending ||
desaturateMutation.isPending ||
rotateMutation.isPending;
return (
<div className="min-h-screen p-8">
<div className="max-w-7xl mx-auto space-y-8">
<div>
<h1 className="text-4xl font-bold mb-2">Batch Operations</h1>
<p className="text-muted-foreground">
Process multiple colors at once with manipulation operations
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Input */}
<div className="space-y-6">
<div className="p-6 border rounded-lg bg-card">
<h2 className="text-xl font-semibold mb-4">Input Colors</h2>
<p className="text-sm text-muted-foreground mb-4">
Enter colors (one per line or comma-separated). Supports hex format.
</p>
<textarea
value={inputColors}
onChange={(e) => setInputColors(e.target.value)}
placeholder="#ff0099, #00ff99, #9900ff&#10;#ff5533&#10;#3355ff"
className="w-full h-48 p-3 border rounded-lg bg-background font-mono text-sm"
/>
<p className="text-xs text-muted-foreground mt-2">
{parseColors(inputColors).length} valid colors found
</p>
</div>
<div className="p-6 border rounded-lg bg-card">
<h2 className="text-xl font-semibold mb-4">Operation</h2>
<div className="space-y-4">
<Select
label="Operation"
value={operation}
onChange={(e) => setOperation(e.target.value as Operation)}
>
<option value="lighten">Lighten</option>
<option value="darken">Darken</option>
<option value="saturate">Saturate</option>
<option value="desaturate">Desaturate</option>
<option value="rotate">Rotate Hue</option>
</Select>
<div>
<label className="text-sm font-medium mb-2 block">
Amount: {operation === 'rotate' ? (amount * 360).toFixed(0) + '°' : (amount * 100).toFixed(0) + '%'}
</label>
<Input
type="range"
min="0"
max="1"
step="0.01"
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value))}
/>
</div>
<Button
onClick={handleProcess}
disabled={isPending || parseColors(inputColors).length === 0}
className="w-full"
>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Process Colors
</>
)}
</Button>
</div>
</div>
</div>
{/* Output */}
<div className="space-y-6">
{outputColors.length > 0 ? (
<>
<div className="p-6 border rounded-lg bg-card">
<h2 className="text-xl font-semibold mb-4">
Output Colors ({outputColors.length})
</h2>
<PaletteGrid colors={outputColors} />
</div>
<div className="p-6 border rounded-lg bg-card">
<ExportMenu colors={outputColors} />
</div>
</>
) : (
<div className="p-12 border rounded-lg bg-card text-center text-muted-foreground">
<Download className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Enter colors and click Process to see results</p>
</div>
)}
</div>
</div>
</div>
</div>
);
}