Files
thrilltrack-explorer/src-old/components/admin/editors/TechnicalSpecsEditor.tsx

288 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Plus, Trash2 } from "lucide-react";
import { useState } from "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";
import { useUnitPreferences } from "@/hooks/useUnitPreferences";
import { toast } from "sonner";
import {
convertValueToMetric,
convertValueFromMetric,
detectUnitType,
getMetricUnit,
getDisplayUnit
} from "@/lib/units";
import { validateMetricUnit, METRIC_UNITS } from "@/lib/unitValidation";
import { getErrorMessage } from "@/lib/errorHandler";
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'];
export function TechnicalSpecsEditor({
specs,
onChange,
categories = DEFAULT_CATEGORIES,
commonSpecs = []
}: TechnicalSpecsEditorProps) {
const { preferences } = useUnitPreferences();
const [unitErrors, setUnitErrors] = useState<Record<number, string>>({});
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: string | number | boolean | null | undefined) => {
const newSpecs = [...specs];
// Ensure unit is metric when updating unit field
if (field === 'unit' && value && typeof value === 'string') {
try {
validateMetricUnit(value, 'Unit');
newSpecs[index] = { ...newSpecs[index], unit: value };
// Clear error for this index
setUnitErrors(prev => {
const updated = { ...prev };
delete updated[index];
return updated;
});
} catch (error: unknown) {
const message = getErrorMessage(error);
toast.error(message);
// Store error for visual feedback
setUnitErrors(prev => ({ ...prev, [index]: message }));
return;
}
} else {
newSpecs[index] = { ...newSpecs[index], [field]: value };
}
onChange(newSpecs);
};
// Get display value (convert from metric to user's preferred units)
const getDisplayValue = (spec: TechnicalSpec): string => {
if (!spec.spec_value || !spec.unit || spec.spec_type !== 'number') return spec.spec_value;
const numValue = parseFloat(spec.spec_value);
if (isNaN(numValue)) return spec.spec_value;
const unitType = detectUnitType(spec.unit);
if (unitType === 'unknown') return spec.spec_value;
// spec.unit is the metric unit (e.g., "km/h")
// Get the display unit based on user preference (e.g., "mph" for imperial)
const displayUnit = getDisplayUnit(spec.unit, preferences.measurement_system);
// Convert from metric to display unit
const displayValue = convertValueFromMetric(numValue, displayUnit, spec.unit);
return String(displayValue);
};
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={getDisplayValue(spec)}
onChange={(e) => {
const inputValue = e.target.value;
const numValue = parseFloat(inputValue);
// If type is number and unit is recognized, convert to metric for storage
if (spec.spec_type === 'number' && spec.unit && !isNaN(numValue)) {
// Determine what unit the user is entering (based on their preference)
const displayUnit = getDisplayUnit(spec.unit, preferences.measurement_system);
// Convert from user's input unit to metric for storage
const metricValue = convertValueToMetric(numValue, displayUnit);
updateSpec(index, 'spec_value', String(metricValue));
} else {
updateSpec(index, 'spec_value', inputValue);
}
}}
placeholder="Value"
type={spec.spec_type === 'number' ? 'number' : 'text'}
/>
{spec.spec_type === 'number' && spec.unit && detectUnitType(spec.unit) !== 'unknown' && (
<p className="text-xs text-muted-foreground mt-1">
Enter in {getDisplayUnit(spec.unit, preferences.measurement_system)}
</p>
)}
</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}`}
className={unitErrors[index] ? 'border-destructive' : ''}
/>
<datalist id={`units-${index}`}>
{METRIC_UNITS.map(u => <option key={u} value={u} />)}
</datalist>
{unitErrors[index] && (
<p className="text-xs text-destructive mt-1">{unitErrors[index]}</p>
)}
<p className="text-xs text-muted-foreground mt-1">
Metric units only
</p>
</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>
);
}
/**
* Validates technical specs before submission
*/
export function validateTechnicalSpecs(specs: TechnicalSpec[]): { valid: boolean; errors: string[] } {
const errors: string[] = [];
specs.forEach((spec, index) => {
if (!spec.spec_name?.trim()) {
errors.push(`Spec ${index + 1}: Name is required`);
}
if (!spec.spec_value?.trim()) {
errors.push(`Spec ${index + 1} (${spec.spec_name}): Value is required`);
}
if (spec.unit) {
try {
validateMetricUnit(spec.unit, `Spec ${index + 1} (${spec.spec_name})`);
} catch (error: unknown) {
errors.push(getErrorMessage(error));
}
}
});
return { valid: errors.length === 0, errors };
}