diff --git a/src/components/admin/editors/CoasterStatsEditor.tsx b/src/components/admin/editors/CoasterStatsEditor.tsx
new file mode 100644
index 00000000..d1c4ccae
--- /dev/null
+++ b/src/components/admin/editors/CoasterStatsEditor.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {stats.length === 0 ? (
+
+ No statistics added yet. Add a common stat or create a custom one.
+
+ ) : (
+
+ {stats.map((stat, index) => (
+
+
+
+
+ updateStat(index, 'stat_name', e.target.value)}
+ placeholder="e.g., Max Speed"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/admin/editors/FormerNamesEditor.tsx b/src/components/admin/editors/FormerNamesEditor.tsx
new file mode 100644
index 00000000..ba4da1ac
--- /dev/null
+++ b/src/components/admin/editors/FormerNamesEditor.tsx
@@ -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 (
+
+
+
+
+
+ Current name: {currentName}
+
+
+
+
+
+ {names.length === 0 ? (
+
+ No former names recorded. This entity has kept its original name.
+
+ ) : (
+
+ {sortedNames.map((name, displayIndex) => {
+ const actualIndex = names.findIndex(n => n === name);
+ return (
+
+
+
+
+
+
+ updateName(actualIndex, 'former_name', e.target.value)}
+ placeholder="Enter former name..."
+ />
+
+
+
+
+
+ updateName(actualIndex, 'date_changed', date)}
+ />
+
+
+
+
+ updateName(actualIndex, 'from_year', e.target.value ? parseInt(e.target.value) : undefined)}
+ placeholder="Year"
+ min="1800"
+ max={new Date().getFullYear()}
+ />
+
+
+
+
+ updateName(actualIndex, 'to_year', e.target.value ? parseInt(e.target.value) : undefined)}
+ placeholder="Year"
+ min="1800"
+ max={new Date().getFullYear()}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {name.date_changed && (
+
+
+
+ Changed on {new Date(name.date_changed).toLocaleDateString()}
+ {name.from_year && name.to_year && ` (used from ${name.from_year} to ${name.to_year})`}
+
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ {names.length > 0 && (
+
+
+
Name Timeline:
+
+ {sortedNames
+ .filter(n => n.former_name)
+ .map((name, idx) => (
+
+
+
{name.former_name}
+ {name.from_year && name.to_year && (
+
+ ({name.from_year} - {name.to_year})
+
+ )}
+
+ ))}
+
+
+
{currentName}
+
(Current)
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/admin/editors/TechnicalSpecsEditor.tsx b/src/components/admin/editors/TechnicalSpecsEditor.tsx
new file mode 100644
index 00000000..c9569b82
--- /dev/null
+++ b/src/components/admin/editors/TechnicalSpecsEditor.tsx
@@ -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 (
+
+
+
+
+
+
+ {specs.length === 0 ? (
+
+ No specifications added yet. Click "Add Specification" to get started.
+
+ ) : (
+
+ {specs.map((spec, index) => (
+
+
+
+
+ updateSpec(index, 'spec_name', e.target.value)}
+ placeholder="e.g., Track Material"
+ list={`common-specs-${index}`}
+ />
+ {commonSpecs.length > 0 && (
+
+ )}
+
+
+
+
+ updateSpec(index, 'spec_value', e.target.value)}
+ placeholder="Value"
+ type={spec.spec_type === 'number' ? 'number' : 'text'}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateSpec(index, 'unit', e.target.value)}
+ placeholder="Unit"
+ list={`units-${index}`}
+ />
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}