diff --git a/app/page.tsx b/app/page.tsx
index e3dada3..3671909 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,23 +1,19 @@
+import MainConverter from '@/components/converter/MainConverter';
+
export default function Home() {
return (
-
+
-
-
+
-
-
- Coming soon: Real-time bidirectional conversion with innovative UX
-
-
-
+
);
diff --git a/components/converter/MainConverter.tsx b/components/converter/MainConverter.tsx
new file mode 100644
index 0000000..4cd58d5
--- /dev/null
+++ b/components/converter/MainConverter.tsx
@@ -0,0 +1,144 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import {
+ getAllMeasures,
+ getUnitsForMeasure,
+ convertToAll,
+ formatMeasureName,
+ getCategoryColor,
+ type Measure,
+ type ConversionResult,
+} from '@/lib/units';
+import { parseNumberInput, formatNumber } from '@/lib/utils';
+
+export default function MainConverter() {
+ const [selectedMeasure, setSelectedMeasure] = useState
('length');
+ const [selectedUnit, setSelectedUnit] = useState('m');
+ const [inputValue, setInputValue] = useState('1');
+ const [conversions, setConversions] = useState([]);
+
+ const measures = getAllMeasures();
+ const units = getUnitsForMeasure(selectedMeasure);
+
+ // Update conversions when input changes
+ useEffect(() => {
+ const numValue = parseNumberInput(inputValue);
+ if (numValue !== null && selectedUnit) {
+ const results = convertToAll(numValue, selectedUnit);
+ setConversions(results);
+ } else {
+ setConversions([]);
+ }
+ }, [inputValue, selectedUnit]);
+
+ // Update selected unit when measure changes
+ useEffect(() => {
+ const availableUnits = getUnitsForMeasure(selectedMeasure);
+ if (availableUnits.length > 0) {
+ setSelectedUnit(availableUnits[0]);
+ }
+ }, [selectedMeasure]);
+
+ return (
+
+ {/* Category Selection */}
+
+
+ Select Category
+
+
+
+ {measures.map((measure) => (
+
+ ))}
+
+
+
+
+ {/* Input Section */}
+
+
+ Convert {formatMeasureName(selectedMeasure)}
+
+
+
+
+
+ setInputValue(e.target.value)}
+ placeholder="Enter value"
+ className="text-lg"
+ />
+
+
+
+
+
+
+
+
+
+ {/* Results */}
+
+
+ Conversions
+
+
+
+ {conversions.map((conversion) => (
+
+
+ {conversion.unitInfo.plural}
+
+
+ {formatNumber(conversion.value)}
+
+
+ {conversion.unit}
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..662a67f
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes {
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
+ size?: 'default' | 'sm' | 'lg' | 'icon';
+}
+
+const Button = React.forwardRef(
+ ({ className, variant = 'default', size = 'default', ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Button.displayName = 'Button';
+
+export { Button };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..81bd82d
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..34af7d8
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/lib/units.ts b/lib/units.ts
new file mode 100644
index 0000000..217c292
--- /dev/null
+++ b/lib/units.ts
@@ -0,0 +1,218 @@
+/**
+ * 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 = {
+ 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 = {
+ 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;
+}
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..7b0a1ad
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,106 @@
+/**
+ * Utility functions for the application
+ */
+
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+/**
+ * Merge Tailwind CSS classes with clsx
+ */
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+/**
+ * Format a number for display with proper precision
+ */
+export function formatNumber(
+ value: number,
+ options: {
+ maxDecimals?: number;
+ minDecimals?: number;
+ notation?: 'standard' | 'scientific' | 'engineering' | 'compact';
+ } = {}
+): string {
+ const {
+ maxDecimals = 6,
+ minDecimals = 0,
+ notation = 'standard',
+ } = options;
+
+ // Handle edge cases
+ if (!isFinite(value)) return value.toString();
+ if (value === 0) return '0';
+
+ // Use scientific notation for very large or very small numbers
+ const absValue = Math.abs(value);
+ const useScientific =
+ notation === 'scientific' ||
+ (notation === 'standard' && (absValue >= 1e10 || absValue < 1e-6));
+
+ if (useScientific) {
+ return value.toExponential(maxDecimals);
+ }
+
+ // Format with appropriate decimal places
+ const formatted = new Intl.NumberFormat('en-US', {
+ minimumFractionDigits: minDecimals,
+ maximumFractionDigits: maxDecimals,
+ notation: notation === 'compact' ? 'compact' : 'standard',
+ }).format(value);
+
+ return formatted;
+}
+
+/**
+ * Debounce function for input handling
+ */
+export function debounce any>(
+ func: T,
+ wait: number
+): (...args: Parameters) => void {
+ let timeout: NodeJS.Timeout | null = null;
+
+ return function executedFunction(...args: Parameters) {
+ const later = () => {
+ timeout = null;
+ func(...args);
+ };
+
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+}
+
+/**
+ * Parse a number input string
+ */
+export function parseNumberInput(input: string): number | null {
+ if (!input || input.trim() === '') return null;
+
+ // Remove spaces and replace comma with dot
+ const cleaned = input.replace(/\s/g, '').replace(',', '.');
+
+ const parsed = parseFloat(cleaned);
+
+ return isNaN(parsed) ? null : parsed;
+}
+
+/**
+ * Get relative time from timestamp
+ */
+export function getRelativeTime(timestamp: number): string {
+ const now = Date.now();
+ const diff = now - timestamp;
+
+ const seconds = Math.floor(diff / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+
+ if (days > 0) return `${days}d ago`;
+ if (hours > 0) return `${hours}h ago`;
+ if (minutes > 0) return `${minutes}m ago`;
+ return 'just now';
+}
diff --git a/package.json b/package.json
index cdc576d..98a7378 100644
--- a/package.json
+++ b/package.json
@@ -9,10 +9,12 @@
"lint": "next lint"
},
"dependencies": {
+ "clsx": "^2.1.1",
"convert-units": "^2.3.4",
"next": "^16.0.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 51b9891..b554879 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
convert-units:
specifier: ^2.3.4
version: 2.3.4
@@ -20,6 +23,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.2.0(react@19.2.0)
+ tailwind-merge:
+ specifier: ^3.3.1
+ version: 3.3.1
devDependencies:
'@tailwindcss/postcss':
specifier: ^4.1.17
@@ -802,6 +808,10 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1855,6 +1865,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ tailwind-merge@3.3.1:
+ resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
+
tailwindcss@4.1.17:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
@@ -2721,6 +2734,8 @@ snapshots:
client-only@0.0.1: {}
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -4001,6 +4016,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ tailwind-merge@3.3.1: {}
+
tailwindcss@4.1.17: {}
tapable@2.3.0: {}