mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 14:51:13 -05:00
Fix unit conversions in forms
This commit is contained in:
@@ -5,6 +5,13 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { useUnitPreferences } from "@/hooks/useUnitPreferences";
|
||||||
|
import {
|
||||||
|
convertValueToMetric,
|
||||||
|
convertValueFromMetric,
|
||||||
|
detectUnitType,
|
||||||
|
getMetricUnit
|
||||||
|
} from "@/lib/units";
|
||||||
|
|
||||||
interface CoasterStat {
|
interface CoasterStat {
|
||||||
stat_name: string;
|
stat_name: string;
|
||||||
@@ -38,6 +45,7 @@ export function CoasterStatsEditor({
|
|||||||
onChange,
|
onChange,
|
||||||
categories = DEFAULT_CATEGORIES
|
categories = DEFAULT_CATEGORIES
|
||||||
}: CoasterStatsEditorProps) {
|
}: CoasterStatsEditorProps) {
|
||||||
|
const { preferences } = useUnitPreferences();
|
||||||
|
|
||||||
const addStat = () => {
|
const addStat = () => {
|
||||||
onChange([
|
onChange([
|
||||||
@@ -78,6 +86,23 @@ export function CoasterStatsEditor({
|
|||||||
onChange(newStats);
|
onChange(newStats);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get display value (convert from metric if needed)
|
||||||
|
const getDisplayValue = (stat: CoasterStat): string => {
|
||||||
|
if (!stat.stat_value || !stat.unit) return String(stat.stat_value || '');
|
||||||
|
|
||||||
|
const numValue = Number(stat.stat_value);
|
||||||
|
if (isNaN(numValue)) return String(stat.stat_value);
|
||||||
|
|
||||||
|
const unitType = detectUnitType(stat.unit);
|
||||||
|
if (unitType === 'unknown') return String(stat.stat_value);
|
||||||
|
|
||||||
|
// Assume stored value is in metric, convert to user's preferred units
|
||||||
|
const metricUnit = getMetricUnit(stat.unit);
|
||||||
|
const displayValue = convertValueFromMetric(numValue, stat.unit, metricUnit);
|
||||||
|
|
||||||
|
return String(displayValue);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -129,10 +154,27 @@ export function CoasterStatsEditor({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={stat.stat_value}
|
value={getDisplayValue(stat)}
|
||||||
onChange={(e) => updateStat(index, 'stat_value', parseFloat(e.target.value) || 0)}
|
onChange={(e) => {
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
// If unit is recognized, convert to metric for storage
|
||||||
|
if (stat.unit) {
|
||||||
|
const numValue = parseFloat(inputValue);
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
const metricValue = convertValueToMetric(numValue, stat.unit);
|
||||||
|
updateStat(index, 'stat_value', metricValue);
|
||||||
|
} else {
|
||||||
|
updateStat(index, 'stat_value', 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateStat(index, 'stat_value', parseFloat(inputValue) || 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Using {preferences.measurement_system} units
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">Unit</Label>
|
<Label className="text-xs">Unit</Label>
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { useUnitPreferences } from "@/hooks/useUnitPreferences";
|
||||||
|
import {
|
||||||
|
convertValueToMetric,
|
||||||
|
convertValueFromMetric,
|
||||||
|
detectUnitType,
|
||||||
|
getMetricUnit
|
||||||
|
} from "@/lib/units";
|
||||||
|
|
||||||
interface TechnicalSpec {
|
interface TechnicalSpec {
|
||||||
spec_name: string;
|
spec_name: string;
|
||||||
@@ -30,6 +37,7 @@ export function TechnicalSpecsEditor({
|
|||||||
categories = DEFAULT_CATEGORIES,
|
categories = DEFAULT_CATEGORIES,
|
||||||
commonSpecs = []
|
commonSpecs = []
|
||||||
}: TechnicalSpecsEditorProps) {
|
}: TechnicalSpecsEditorProps) {
|
||||||
|
const { preferences } = useUnitPreferences();
|
||||||
|
|
||||||
const addSpec = () => {
|
const addSpec = () => {
|
||||||
onChange([
|
onChange([
|
||||||
@@ -57,6 +65,23 @@ export function TechnicalSpecsEditor({
|
|||||||
onChange(newSpecs);
|
onChange(newSpecs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get display value (convert from metric if needed)
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Assume stored value is in metric, convert to user's preferred units
|
||||||
|
const metricUnit = getMetricUnit(spec.unit);
|
||||||
|
const displayValue = convertValueFromMetric(numValue, spec.unit, metricUnit);
|
||||||
|
|
||||||
|
return String(displayValue);
|
||||||
|
};
|
||||||
|
|
||||||
const moveSpec = (index: number, direction: 'up' | 'down') => {
|
const moveSpec = (index: number, direction: 'up' | 'down') => {
|
||||||
if ((direction === 'up' && index === 0) || (direction === 'down' && index === specs.length - 1)) {
|
if ((direction === 'up' && index === 0) || (direction === 'down' && index === specs.length - 1)) {
|
||||||
return;
|
return;
|
||||||
@@ -107,11 +132,30 @@ export function TechnicalSpecsEditor({
|
|||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">Value</Label>
|
<Label className="text-xs">Value</Label>
|
||||||
<Input
|
<Input
|
||||||
value={spec.spec_value}
|
value={getDisplayValue(spec)}
|
||||||
onChange={(e) => updateSpec(index, 'spec_value', e.target.value)}
|
onChange={(e) => {
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
// If type is number and unit is recognized, convert to metric for storage
|
||||||
|
if (spec.spec_type === 'number' && spec.unit) {
|
||||||
|
const numValue = parseFloat(inputValue);
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
const metricValue = convertValueToMetric(numValue, spec.unit);
|
||||||
|
updateSpec(index, 'spec_value', String(metricValue));
|
||||||
|
} else {
|
||||||
|
updateSpec(index, 'spec_value', inputValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateSpec(index, 'spec_value', inputValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
type={spec.spec_type === 'number' ? 'number' : 'text'}
|
type={spec.spec_type === 'number' ? 'number' : 'text'}
|
||||||
/>
|
/>
|
||||||
|
{spec.spec_type === 'number' && spec.unit && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Using {preferences.measurement_system} units
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
129
src/lib/units.ts
129
src/lib/units.ts
@@ -76,3 +76,132 @@ export const IMPERIAL_COUNTRIES = ['US', 'LR', 'MM'];
|
|||||||
export function getMeasurementSystemFromCountry(countryCode: string): MeasurementSystem {
|
export function getMeasurementSystemFromCountry(countryCode: string): MeasurementSystem {
|
||||||
return IMPERIAL_COUNTRIES.includes(countryCode.toUpperCase()) ? 'imperial' : 'metric';
|
return IMPERIAL_COUNTRIES.includes(countryCode.toUpperCase()) ? 'imperial' : 'metric';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unit type detection
|
||||||
|
export type UnitType = 'speed' | 'distance' | 'height' | 'weight' | 'unknown';
|
||||||
|
|
||||||
|
export function detectUnitType(unit: string): UnitType {
|
||||||
|
const normalized = unit.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Speed units
|
||||||
|
if (['km/h', 'kmh', 'kph', 'mph', 'm/s', 'ms'].includes(normalized)) {
|
||||||
|
return 'speed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance units (meters/feet)
|
||||||
|
if (['m', 'meter', 'meters', 'metre', 'metres', 'ft', 'feet', 'foot'].includes(normalized)) {
|
||||||
|
return 'distance';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height units (cm/inches)
|
||||||
|
if (['cm', 'centimeter', 'centimeters', 'in', 'inch', 'inches'].includes(normalized)) {
|
||||||
|
return 'height';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight units
|
||||||
|
if (['kg', 'kilogram', 'kilograms', 'lb', 'lbs', 'pound', 'pounds'].includes(normalized)) {
|
||||||
|
return 'weight';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert any value to metric based on its unit
|
||||||
|
export function convertValueToMetric(value: number, unit: string): number {
|
||||||
|
const normalized = unit.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Speed conversions to km/h
|
||||||
|
if (normalized === 'mph') {
|
||||||
|
return Math.round(value / 0.621371);
|
||||||
|
}
|
||||||
|
if (['m/s', 'ms'].includes(normalized)) {
|
||||||
|
return Math.round(value * 3.6);
|
||||||
|
}
|
||||||
|
if (['km/h', 'kmh', 'kph'].includes(normalized)) {
|
||||||
|
return Math.round(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance conversions to meters
|
||||||
|
if (['ft', 'feet', 'foot'].includes(normalized)) {
|
||||||
|
return Math.round(value / 3.28084);
|
||||||
|
}
|
||||||
|
if (['m', 'meter', 'meters', 'metre', 'metres'].includes(normalized)) {
|
||||||
|
return Math.round(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height conversions to cm
|
||||||
|
if (['in', 'inch', 'inches'].includes(normalized)) {
|
||||||
|
return Math.round(value / 0.393701);
|
||||||
|
}
|
||||||
|
if (['cm', 'centimeter', 'centimeters'].includes(normalized)) {
|
||||||
|
return Math.round(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight conversions to kg
|
||||||
|
if (['lb', 'lbs', 'pound', 'pounds'].includes(normalized)) {
|
||||||
|
return Math.round(value / 2.20462);
|
||||||
|
}
|
||||||
|
if (['kg', 'kilogram', 'kilograms'].includes(normalized)) {
|
||||||
|
return Math.round(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown unit, return as-is
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert metric value to target unit
|
||||||
|
export function convertValueFromMetric(value: number, targetUnit: string, metricUnit: string): number {
|
||||||
|
const normalized = targetUnit.toLowerCase().trim();
|
||||||
|
const metricNormalized = metricUnit.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Speed conversions from km/h
|
||||||
|
if (metricNormalized === 'km/h' || metricNormalized === 'kmh' || metricNormalized === 'kph') {
|
||||||
|
if (normalized === 'mph') {
|
||||||
|
return Math.round(value * 0.621371);
|
||||||
|
}
|
||||||
|
if (normalized === 'm/s' || normalized === 'ms') {
|
||||||
|
return Math.round(value / 3.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance conversions from meters
|
||||||
|
if (metricNormalized === 'm' || metricNormalized === 'meter' || metricNormalized === 'meters') {
|
||||||
|
if (['ft', 'feet', 'foot'].includes(normalized)) {
|
||||||
|
return Math.round(value * 3.28084);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height conversions from cm
|
||||||
|
if (metricNormalized === 'cm' || metricNormalized === 'centimeter' || metricNormalized === 'centimeters') {
|
||||||
|
if (['in', 'inch', 'inches'].includes(normalized)) {
|
||||||
|
return Math.round(value * 0.393701);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight conversions from kg
|
||||||
|
if (metricNormalized === 'kg' || metricNormalized === 'kilogram' || metricNormalized === 'kilograms') {
|
||||||
|
if (['lb', 'lbs', 'pound', 'pounds'].includes(normalized)) {
|
||||||
|
return Math.round(value * 2.20462);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get metric unit for a given unit type
|
||||||
|
export function getMetricUnit(unit: string): string {
|
||||||
|
const unitType = detectUnitType(unit);
|
||||||
|
|
||||||
|
switch (unitType) {
|
||||||
|
case 'speed':
|
||||||
|
return 'km/h';
|
||||||
|
case 'distance':
|
||||||
|
return 'm';
|
||||||
|
case 'height':
|
||||||
|
return 'cm';
|
||||||
|
case 'weight':
|
||||||
|
return 'kg';
|
||||||
|
default:
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user