mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 22:11:24 -05:00
Implement Phase 3B
This commit is contained in:
181
src/components/admin/editors/TechnicalSpecsEditor.tsx
Normal file
181
src/components/admin/editors/TechnicalSpecsEditor.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface TechnicalSpec {
|
||||
spec_name: string;
|
||||
spec_value: string;
|
||||
spec_type: 'string' | 'number' | 'boolean' | 'date';
|
||||
category?: string;
|
||||
unit?: string;
|
||||
display_order: number;
|
||||
}
|
||||
|
||||
interface TechnicalSpecsEditorProps {
|
||||
specs: TechnicalSpec[];
|
||||
onChange: (specs: TechnicalSpec[]) => void;
|
||||
categories?: string[];
|
||||
commonSpecs?: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_CATEGORIES = ['Performance', 'Safety', 'Design', 'Capacity', 'Technical', 'Other'];
|
||||
const COMMON_UNITS = ['m', 'km/h', 'mph', 'ft', 'seconds', 'minutes', 'kg', 'lbs', 'passengers', '%'];
|
||||
|
||||
export function TechnicalSpecsEditor({
|
||||
specs,
|
||||
onChange,
|
||||
categories = DEFAULT_CATEGORIES,
|
||||
commonSpecs = []
|
||||
}: TechnicalSpecsEditorProps) {
|
||||
|
||||
const addSpec = () => {
|
||||
onChange([
|
||||
...specs,
|
||||
{
|
||||
spec_name: '',
|
||||
spec_value: '',
|
||||
spec_type: 'string',
|
||||
category: categories[0],
|
||||
unit: '',
|
||||
display_order: specs.length
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
const removeSpec = (index: number) => {
|
||||
const newSpecs = specs.filter((_, i) => i !== index);
|
||||
// Reorder display_order
|
||||
onChange(newSpecs.map((spec, i) => ({ ...spec, display_order: i })));
|
||||
};
|
||||
|
||||
const updateSpec = (index: number, field: keyof TechnicalSpec, value: any) => {
|
||||
const newSpecs = [...specs];
|
||||
newSpecs[index] = { ...newSpecs[index], [field]: value };
|
||||
onChange(newSpecs);
|
||||
};
|
||||
|
||||
const moveSpec = (index: number, direction: 'up' | 'down') => {
|
||||
if ((direction === 'up' && index === 0) || (direction === 'down' && index === specs.length - 1)) {
|
||||
return;
|
||||
}
|
||||
const newSpecs = [...specs];
|
||||
const swapIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
[newSpecs[index], newSpecs[swapIndex]] = [newSpecs[swapIndex], newSpecs[index]];
|
||||
// Update display_order
|
||||
newSpecs[index].display_order = index;
|
||||
newSpecs[swapIndex].display_order = swapIndex;
|
||||
onChange(newSpecs);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Technical Specifications</Label>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addSpec}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Specification
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{specs.length === 0 ? (
|
||||
<Card className="p-6 text-center text-muted-foreground">
|
||||
No specifications added yet. Click "Add Specification" to get started.
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{specs.map((spec, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3">
|
||||
<div className="lg:col-span-2">
|
||||
<Label className="text-xs">Specification Name</Label>
|
||||
<Input
|
||||
value={spec.spec_name}
|
||||
onChange={(e) => updateSpec(index, 'spec_name', e.target.value)}
|
||||
placeholder="e.g., Track Material"
|
||||
list={`common-specs-${index}`}
|
||||
/>
|
||||
{commonSpecs.length > 0 && (
|
||||
<datalist id={`common-specs-${index}`}>
|
||||
{commonSpecs.map(s => <option key={s} value={s} />)}
|
||||
</datalist>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">Value</Label>
|
||||
<Input
|
||||
value={spec.spec_value}
|
||||
onChange={(e) => updateSpec(index, 'spec_value', e.target.value)}
|
||||
placeholder="Value"
|
||||
type={spec.spec_type === 'number' ? 'number' : 'text'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">Type</Label>
|
||||
<Select
|
||||
value={spec.spec_type}
|
||||
onValueChange={(value) => updateSpec(index, 'spec_type', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">Text</SelectItem>
|
||||
<SelectItem value="number">Number</SelectItem>
|
||||
<SelectItem value="boolean">Yes/No</SelectItem>
|
||||
<SelectItem value="date">Date</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">Category</Label>
|
||||
<Select
|
||||
value={spec.category || ''}
|
||||
onValueChange={(value) => updateSpec(index, 'category', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<Label className="text-xs">Unit</Label>
|
||||
<Input
|
||||
value={spec.unit || ''}
|
||||
onChange={(e) => updateSpec(index, 'unit', e.target.value)}
|
||||
placeholder="Unit"
|
||||
list={`units-${index}`}
|
||||
/>
|
||||
<datalist id={`units-${index}`}>
|
||||
{COMMON_UNITS.map(u => <option key={u} value={u} />)}
|
||||
</datalist>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeSpec(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user