mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 20:51:13 -05:00
Implement Phase 3B
This commit is contained in:
193
src/components/admin/editors/CoasterStatsEditor.tsx
Normal file
193
src/components/admin/editors/CoasterStatsEditor.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
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 { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface CoasterStat {
|
||||||
|
stat_name: string;
|
||||||
|
stat_value: number;
|
||||||
|
unit?: string;
|
||||||
|
category?: string;
|
||||||
|
description?: string;
|
||||||
|
display_order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoasterStatsEditorProps {
|
||||||
|
stats: CoasterStat[];
|
||||||
|
onChange: (stats: CoasterStat[]) => void;
|
||||||
|
categories?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CATEGORIES = ['Speed', 'Height', 'Length', 'Forces', 'Capacity', 'Duration', 'Other'];
|
||||||
|
const COMMON_STATS = [
|
||||||
|
{ name: 'Max Speed', unit: 'km/h', category: 'Speed' },
|
||||||
|
{ name: 'Max Height', unit: 'm', category: 'Height' },
|
||||||
|
{ name: 'Drop Height', unit: 'm', category: 'Height' },
|
||||||
|
{ name: 'Track Length', unit: 'm', category: 'Length' },
|
||||||
|
{ name: 'Max G-Force', unit: 'G', category: 'Forces' },
|
||||||
|
{ name: 'Max Negative G-Force', unit: 'G', category: 'Forces' },
|
||||||
|
{ name: 'Ride Duration', unit: 'seconds', category: 'Duration' },
|
||||||
|
{ name: 'Inversions', unit: 'count', category: 'Other' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function CoasterStatsEditor({
|
||||||
|
stats,
|
||||||
|
onChange,
|
||||||
|
categories = DEFAULT_CATEGORIES
|
||||||
|
}: CoasterStatsEditorProps) {
|
||||||
|
|
||||||
|
const addStat = () => {
|
||||||
|
onChange([
|
||||||
|
...stats,
|
||||||
|
{
|
||||||
|
stat_name: '',
|
||||||
|
stat_value: 0,
|
||||||
|
unit: '',
|
||||||
|
category: categories[0],
|
||||||
|
description: '',
|
||||||
|
display_order: stats.length
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCommonStat = (commonStat: typeof COMMON_STATS[0]) => {
|
||||||
|
onChange([
|
||||||
|
...stats,
|
||||||
|
{
|
||||||
|
stat_name: commonStat.name,
|
||||||
|
stat_value: 0,
|
||||||
|
unit: commonStat.unit,
|
||||||
|
category: commonStat.category,
|
||||||
|
description: '',
|
||||||
|
display_order: stats.length
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeStat = (index: number) => {
|
||||||
|
const newStats = stats.filter((_, i) => i !== index);
|
||||||
|
onChange(newStats.map((stat, i) => ({ ...stat, display_order: i })));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStat = (index: number, field: keyof CoasterStat, value: any) => {
|
||||||
|
const newStats = [...stats];
|
||||||
|
newStats[index] = { ...newStats[index], [field]: value };
|
||||||
|
onChange(newStats);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>Coaster Statistics</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select onValueChange={(value) => {
|
||||||
|
const commonStat = COMMON_STATS.find(s => s.name === value);
|
||||||
|
if (commonStat) addCommonStat(commonStat);
|
||||||
|
}}>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="Add common stat..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{COMMON_STATS.map(stat => (
|
||||||
|
<SelectItem key={stat.name} value={stat.name}>
|
||||||
|
{stat.name} ({stat.unit})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addStat}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Custom
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stats.length === 0 ? (
|
||||||
|
<Card className="p-6 text-center text-muted-foreground">
|
||||||
|
No statistics added yet. Add a common stat or create a custom one.
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<Card key={index} className="p-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Statistic Name</Label>
|
||||||
|
<Input
|
||||||
|
value={stat.stat_name}
|
||||||
|
onChange={(e) => updateStat(index, 'stat_name', e.target.value)}
|
||||||
|
placeholder="e.g., Max Speed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Value</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={stat.stat_value}
|
||||||
|
onChange={(e) => updateStat(index, 'stat_value', parseFloat(e.target.value) || 0)}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Unit</Label>
|
||||||
|
<Input
|
||||||
|
value={stat.unit || ''}
|
||||||
|
onChange={(e) => updateStat(index, 'unit', e.target.value)}
|
||||||
|
placeholder="km/h, m, G..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Category</Label>
|
||||||
|
<Select
|
||||||
|
value={stat.category || ''}
|
||||||
|
onValueChange={(value) => updateStat(index, 'category', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeStat(index)}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive mr-2" />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Label className="text-xs">Description (optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={stat.description || ''}
|
||||||
|
onChange={(e) => updateStat(index, 'description', e.target.value)}
|
||||||
|
placeholder="Additional context about this statistic..."
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
194
src/components/admin/editors/FormerNamesEditor.tsx
Normal file
194
src/components/admin/editors/FormerNamesEditor.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { Plus, Trash2, Calendar } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { DatePicker } from "@/components/ui/date-picker";
|
||||||
|
|
||||||
|
interface FormerName {
|
||||||
|
former_name: string;
|
||||||
|
date_changed?: Date | null;
|
||||||
|
reason?: string;
|
||||||
|
from_year?: number;
|
||||||
|
to_year?: number;
|
||||||
|
order_index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormerNamesEditorProps {
|
||||||
|
names: FormerName[];
|
||||||
|
onChange: (names: FormerName[]) => void;
|
||||||
|
currentName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormerNamesEditor({ names, onChange, currentName }: FormerNamesEditorProps) {
|
||||||
|
|
||||||
|
const addName = () => {
|
||||||
|
onChange([
|
||||||
|
...names,
|
||||||
|
{
|
||||||
|
former_name: '',
|
||||||
|
date_changed: null,
|
||||||
|
reason: '',
|
||||||
|
order_index: names.length
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeName = (index: number) => {
|
||||||
|
const newNames = names.filter((_, i) => i !== index);
|
||||||
|
onChange(newNames.map((name, i) => ({ ...name, order_index: i })));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateName = (index: number, field: keyof FormerName, value: any) => {
|
||||||
|
const newNames = [...names];
|
||||||
|
newNames[index] = { ...newNames[index], [field]: value };
|
||||||
|
onChange(newNames);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort names by date_changed (most recent first) for display
|
||||||
|
const sortedNames = [...names].sort((a, b) => {
|
||||||
|
if (!a.date_changed && !b.date_changed) return 0;
|
||||||
|
if (!a.date_changed) return 1;
|
||||||
|
if (!b.date_changed) return -1;
|
||||||
|
return new Date(b.date_changed).getTime() - new Date(a.date_changed).getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label>Former Names</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Current name: <span className="font-medium">{currentName}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={addName}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Former Name
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{names.length === 0 ? (
|
||||||
|
<Card className="p-6 text-center text-muted-foreground">
|
||||||
|
No former names recorded. This entity has kept its original name.
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sortedNames.map((name, displayIndex) => {
|
||||||
|
const actualIndex = names.findIndex(n => n === name);
|
||||||
|
return (
|
||||||
|
<Card key={actualIndex} className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Former Name</Label>
|
||||||
|
<Input
|
||||||
|
value={name.former_name}
|
||||||
|
onChange={(e) => updateName(actualIndex, 'former_name', e.target.value)}
|
||||||
|
placeholder="Enter former name..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Date Changed</Label>
|
||||||
|
<DatePicker
|
||||||
|
date={name.date_changed ? new Date(name.date_changed) : undefined}
|
||||||
|
onSelect={(date) => updateName(actualIndex, 'date_changed', date)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">From Year (optional)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={name.from_year || ''}
|
||||||
|
onChange={(e) => updateName(actualIndex, 'from_year', e.target.value ? parseInt(e.target.value) : undefined)}
|
||||||
|
placeholder="Year"
|
||||||
|
min="1800"
|
||||||
|
max={new Date().getFullYear()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">To Year (optional)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={name.to_year || ''}
|
||||||
|
onChange={(e) => updateName(actualIndex, 'to_year', e.target.value ? parseInt(e.target.value) : undefined)}
|
||||||
|
placeholder="Year"
|
||||||
|
min="1800"
|
||||||
|
max={new Date().getFullYear()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Reason for Change (optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={name.reason || ''}
|
||||||
|
onChange={(e) => updateName(actualIndex, 'reason', e.target.value)}
|
||||||
|
placeholder="Why was the name changed?"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeName(actualIndex)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{name.date_changed && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground border-t pt-2">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span>
|
||||||
|
Changed on {new Date(name.date_changed).toLocaleDateString()}
|
||||||
|
{name.from_year && name.to_year && ` (used from ${name.from_year} to ${name.to_year})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{names.length > 0 && (
|
||||||
|
<Card className="p-4 bg-muted/50">
|
||||||
|
<div className="text-sm">
|
||||||
|
<strong>Name Timeline:</strong>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{sortedNames
|
||||||
|
.filter(n => n.former_name)
|
||||||
|
.map((name, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||||
|
<span className="font-medium">{name.former_name}</span>
|
||||||
|
{name.from_year && name.to_year && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
({name.from_year} - {name.to_year})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||||
|
<span className="font-medium">{currentName}</span>
|
||||||
|
<span className="text-muted-foreground">(Current)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
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