Complete Phase 2 implementation with working unit converter: Core Conversion Engine (lib/units.ts): - Type-safe wrapper for convert-units library - Support for all 23 measures with TypeScript types - getAllMeasures() - Get all available categories - getUnitsForMeasure() - Get units for specific measure - getUnitInfo() - Get detailed unit information - convertUnit() - Convert between two units - convertToAll() - Convert to all compatible units - getCategoryColor() - Get Tailwind color class for measure - formatMeasureName() - Format measure names for display - searchUnits() - Fuzzy search across all units Utility Functions (lib/utils.ts): - cn() - Merge Tailwind classes with clsx and tailwind-merge - formatNumber() - Smart number formatting with scientific notation - debounce() - Debounce helper for inputs - parseNumberInput() - Parse user input to number - getRelativeTime() - Format timestamps UI Components: - Input - Styled input with focus states - Button - 6 variants (default, destructive, outline, secondary, ghost, link) - Card - Card container with header, title, description, content, footer Main Converter Component (components/converter/MainConverter.tsx): - Real-time conversion as user types - Category selection with 23 color-coded buttons - Input field with unit selector - Grid display of all conversions in selected measure - Color-coded result cards with category colors - Responsive layout (1/2/3 column grid) Homepage Updates: - Integrated MainConverter component - Clean header with gradient text - Uses design system colors Dependencies Added: - clsx - Class name utilities - tailwind-merge - Merge Tailwind classes intelligently Features Working: ✓ Select from 23 measurement categories ✓ Real-time conversion to all compatible units ✓ Color-coded categories ✓ Formatted number display ✓ Responsive design 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
219 lines
4.7 KiB
TypeScript
219 lines
4.7 KiB
TypeScript
/**
|
|
* Unit conversion service wrapper for convert-units library
|
|
* Provides type-safe conversion utilities and metadata
|
|
*/
|
|
|
|
import convert from 'convert-units';
|
|
|
|
export type Measure =
|
|
| 'angle'
|
|
| 'apparentPower'
|
|
| 'area'
|
|
| 'current'
|
|
| 'digital'
|
|
| 'each'
|
|
| 'energy'
|
|
| 'frequency'
|
|
| 'illuminance'
|
|
| 'length'
|
|
| 'mass'
|
|
| 'pace'
|
|
| 'partsPer'
|
|
| 'power'
|
|
| 'pressure'
|
|
| 'reactiveEnergy'
|
|
| 'reactivePower'
|
|
| 'speed'
|
|
| 'temperature'
|
|
| 'time'
|
|
| 'voltage'
|
|
| 'volume'
|
|
| 'volumeFlowRate';
|
|
|
|
export interface UnitInfo {
|
|
abbr: string;
|
|
measure: Measure;
|
|
system: 'metric' | 'imperial' | 'bits' | 'bytes' | string;
|
|
singular: string;
|
|
plural: string;
|
|
}
|
|
|
|
export interface ConversionResult {
|
|
value: number;
|
|
unit: string;
|
|
unitInfo: UnitInfo;
|
|
}
|
|
|
|
/**
|
|
* Get all available measures/categories
|
|
*/
|
|
export function getAllMeasures(): Measure[] {
|
|
return convert().measures() as Measure[];
|
|
}
|
|
|
|
/**
|
|
* Get all units for a specific measure
|
|
*/
|
|
export function getUnitsForMeasure(measure: Measure): string[] {
|
|
return convert().possibilities(measure);
|
|
}
|
|
|
|
/**
|
|
* Get detailed information about a unit
|
|
*/
|
|
export function getUnitInfo(unit: string): UnitInfo | null {
|
|
try {
|
|
const description = convert().describe(unit);
|
|
return description as UnitInfo;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a value from one unit to another
|
|
*/
|
|
export function convertUnit(
|
|
value: number,
|
|
fromUnit: string,
|
|
toUnit: string
|
|
): number {
|
|
try {
|
|
return convert(value).from(fromUnit).to(toUnit);
|
|
} catch (error) {
|
|
console.error('Conversion error:', error);
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a value to all compatible units in the same measure
|
|
*/
|
|
export function convertToAll(
|
|
value: number,
|
|
fromUnit: string
|
|
): ConversionResult[] {
|
|
try {
|
|
const unitInfo = getUnitInfo(fromUnit);
|
|
if (!unitInfo) return [];
|
|
|
|
const compatibleUnits = getUnitsForMeasure(unitInfo.measure);
|
|
|
|
return compatibleUnits.map(toUnit => {
|
|
const convertedValue = convertUnit(value, fromUnit, toUnit);
|
|
const toUnitInfo = getUnitInfo(toUnit);
|
|
|
|
return {
|
|
value: convertedValue,
|
|
unit: toUnit,
|
|
unitInfo: toUnitInfo!,
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.error('Conversion error:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get category color for a measure
|
|
*/
|
|
export function getCategoryColor(measure: Measure): string {
|
|
const colorMap: Record<Measure, string> = {
|
|
angle: 'category-angle',
|
|
apparentPower: 'category-apparent-power',
|
|
area: 'category-area',
|
|
current: 'category-current',
|
|
digital: 'category-digital',
|
|
each: 'category-each',
|
|
energy: 'category-energy',
|
|
frequency: 'category-frequency',
|
|
illuminance: 'category-illuminance',
|
|
length: 'category-length',
|
|
mass: 'category-mass',
|
|
pace: 'category-pace',
|
|
partsPer: 'category-parts-per',
|
|
power: 'category-power',
|
|
pressure: 'category-pressure',
|
|
reactiveEnergy: 'category-reactive-energy',
|
|
reactivePower: 'category-reactive-power',
|
|
speed: 'category-speed',
|
|
temperature: 'category-temperature',
|
|
time: 'category-time',
|
|
voltage: 'category-voltage',
|
|
volume: 'category-volume',
|
|
volumeFlowRate: 'category-volume-flow-rate',
|
|
};
|
|
|
|
return colorMap[measure];
|
|
}
|
|
|
|
/**
|
|
* Format measure name for display
|
|
*/
|
|
export function formatMeasureName(measure: Measure): string {
|
|
const nameMap: Record<Measure, string> = {
|
|
angle: 'Angle',
|
|
apparentPower: 'Apparent Power',
|
|
area: 'Area',
|
|
current: 'Current',
|
|
digital: 'Digital Storage',
|
|
each: 'Each',
|
|
energy: 'Energy',
|
|
frequency: 'Frequency',
|
|
illuminance: 'Illuminance',
|
|
length: 'Length',
|
|
mass: 'Mass',
|
|
pace: 'Pace',
|
|
partsPer: 'Parts Per',
|
|
power: 'Power',
|
|
pressure: 'Pressure',
|
|
reactiveEnergy: 'Reactive Energy',
|
|
reactivePower: 'Reactive Power',
|
|
speed: 'Speed',
|
|
temperature: 'Temperature',
|
|
time: 'Time',
|
|
voltage: 'Voltage',
|
|
volume: 'Volume',
|
|
volumeFlowRate: 'Volume Flow Rate',
|
|
};
|
|
|
|
return nameMap[measure];
|
|
}
|
|
|
|
/**
|
|
* Search units by query string (fuzzy search)
|
|
*/
|
|
export function searchUnits(query: string): UnitInfo[] {
|
|
if (!query) return [];
|
|
|
|
const allMeasures = getAllMeasures();
|
|
const results: UnitInfo[] = [];
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
for (const measure of allMeasures) {
|
|
const units = getUnitsForMeasure(measure);
|
|
|
|
for (const unit of units) {
|
|
const info = getUnitInfo(unit);
|
|
if (!info) continue;
|
|
|
|
const searchableText = [
|
|
info.abbr,
|
|
info.singular,
|
|
info.plural,
|
|
measure,
|
|
formatMeasureName(measure),
|
|
]
|
|
.join(' ')
|
|
.toLowerCase();
|
|
|
|
if (searchableText.includes(lowerQuery)) {
|
|
results.push(info);
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|