"use client";
import { useState, useMemo } from "react";
import { ArrowLeftRight, Ruler, Scale, Thermometer, Droplets } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
type UnitCategory = "length" | "weight" | "temperature" | "volume";
type Unit = {
id: string;
name: string;
symbol: string;
toBase: (value: number) => number;
fromBase: (value: number) => number;
};
type Category = {
id: UnitCategory;
name: string;
icon: React.ElementType;
units: Unit[];
baseUnit: string;
};
const categories: Category[] = [
{
id: "length",
name: "Length",
icon: Ruler,
baseUnit: "m",
units: [
{ id: "mm", name: "Millimeters", symbol: "mm", toBase: (v) => v / 1000, fromBase: (v) => v * 1000 },
{ id: "cm", name: "Centimeters", symbol: "cm", toBase: (v) => v / 100, fromBase: (v) => v * 100 },
{ id: "m", name: "Meters", symbol: "m", toBase: (v) => v, fromBase: (v) => v },
{ id: "km", name: "Kilometers", symbol: "km", toBase: (v) => v * 1000, fromBase: (v) => v / 1000 },
{ id: "in", name: "Inches", symbol: "in", toBase: (v) => v * 0.0254, fromBase: (v) => v / 0.0254 },
{ id: "ft", name: "Feet", symbol: "ft", toBase: (v) => v * 0.3048, fromBase: (v) => v / 0.3048 },
{ id: "yd", name: "Yards", symbol: "yd", toBase: (v) => v * 0.9144, fromBase: (v) => v / 0.9144 },
{ id: "mi", name: "Miles", symbol: "mi", toBase: (v) => v * 1609.344, fromBase: (v) => v / 1609.344 },
],
},
{
id: "weight",
name: "Weight",
icon: Scale,
baseUnit: "kg",
units: [
{ id: "mg", name: "Milligrams", symbol: "mg", toBase: (v) => v / 1000000, fromBase: (v) => v * 1000000 },
{ id: "g", name: "Grams", symbol: "g", toBase: (v) => v / 1000, fromBase: (v) => v * 1000 },
{ id: "kg", name: "Kilograms", symbol: "kg", toBase: (v) => v, fromBase: (v) => v },
{ id: "lb", name: "Pounds", symbol: "lb", toBase: (v) => v * 0.453592, fromBase: (v) => v / 0.453592 },
{ id: "oz", name: "Ounces", symbol: "oz", toBase: (v) => v * 0.0283495, fromBase: (v) => v / 0.0283495 },
{ id: "st", name: "Stones", symbol: "st", toBase: (v) => v * 6.35029, fromBase: (v) => v / 6.35029 },
],
},
{
id: "temperature",
name: "Temperature",
icon: Thermometer,
baseUnit: "c",
units: [
{ id: "c", name: "Celsius", symbol: "°C", toBase: (v) => v, fromBase: (v) => v },
{ id: "f", name: "Fahrenheit", symbol: "°F", toBase: (v) => (v - 32) * 5/9, fromBase: (v) => v * 9/5 + 32 },
{ id: "k", name: "Kelvin", symbol: "K", toBase: (v) => v - 273.15, fromBase: (v) => v + 273.15 },
],
},
{
id: "volume",
name: "Volume",
icon: Droplets,
baseUnit: "l",
units: [
{ id: "ml", name: "Milliliters", symbol: "mL", toBase: (v) => v / 1000, fromBase: (v) => v * 1000 },
{ id: "l", name: "Liters", symbol: "L", toBase: (v) => v, fromBase: (v) => v },
{ id: "gal", name: "Gallons (US)", symbol: "gal", toBase: (v) => v * 3.78541, fromBase: (v) => v / 3.78541 },
{ id: "qt", name: "Quarts (US)", symbol: "qt", toBase: (v) => v * 0.946353, fromBase: (v) => v / 0.946353 },
{ id: "pt", name: "Pints (US)", symbol: "pt", toBase: (v) => v * 0.473176, fromBase: (v) => v / 0.473176 },
{ id: "cup", name: "Cups (US)", symbol: "cup", toBase: (v) => v * 0.236588, fromBase: (v) => v / 0.236588 },
{ id: "floz", name: "Fluid Ounces", symbol: "fl oz", toBase: (v) => v * 0.0295735, fromBase: (v) => v / 0.0295735 },
{ id: "tbsp", name: "Tablespoons", symbol: "tbsp", toBase: (v) => v * 0.0147868, fromBase: (v) => v / 0.0147868 },
{ id: "tsp", name: "Teaspoons", symbol: "tsp", toBase: (v) => v * 0.00492892, fromBase: (v) => v / 0.00492892 },
],
},
];
export const title = "React Dialog Block Unit Converter";
export default function DialogUnitConverter() {
const [open, setOpen] = useState(false);
const [categoryId, setCategoryId] = useState<UnitCategory>("length");
const [fromUnitId, setFromUnitId] = useState("km");
const [toUnitId, setToUnitId] = useState("mi");
const [inputValue, setInputValue] = useState("1");
const category = categories.find((c) => c.id === categoryId) || categories[0];
const fromUnit = category.units.find((u) => u.id === fromUnitId) || category.units[0];
const toUnit = category.units.find((u) => u.id === toUnitId) || category.units[1];
const result = useMemo(() => {
const value = parseFloat(inputValue);
if (isNaN(value)) return "";
const baseValue = fromUnit.toBase(value);
const convertedValue = toUnit.fromBase(baseValue);
const decimals = Math.abs(convertedValue) < 0.01 ? 6 :
Math.abs(convertedValue) < 1 ? 4 :
Math.abs(convertedValue) < 100 ? 3 : 2;
return convertedValue.toFixed(decimals).replace(/\.?0+$/, "");
}, [inputValue, fromUnit, toUnit]);
const handleCategoryChange = (newCategory: UnitCategory) => {
setCategoryId(newCategory);
const cat = categories.find((c) => c.id === newCategory);
if (cat) {
setFromUnitId(cat.units[0].id);
setToUnitId(cat.units[1]?.id || cat.units[0].id);
}
setInputValue("1");
};
const handleSwapUnits = () => {
setFromUnitId(toUnitId);
setToUnitId(fromUnitId);
if (result) {
setInputValue(result);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<ArrowLeftRight className="mr-2 h-4 w-4" />
Unit Converter
</Button>
</DialogTrigger>
</div>
<DialogContent style={{ maxWidth: "384px" }}>
<DialogHeader>
<DialogTitle>Unit Converter</DialogTitle>
<DialogDescription>
Convert between units of measurement.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Category Tabs */}
<div className="grid grid-cols-4 gap-1">
{categories.map((cat) => {
const Icon = cat.icon;
return (
<button
key={cat.id}
onClick={() => handleCategoryChange(cat.id)}
className={cn(
"flex flex-col items-center gap-1 rounded-md py-2 text-xs transition-colors",
categoryId === cat.id
? "bg-primary text-primary-foreground"
: "hover:bg-muted"
)}
>
<Icon className="h-4 w-4" />
{cat.name}
</button>
);
})}
</div>
{/* From */}
<div className="space-y-2">
<Label className="text-sm">From</Label>
<div className="flex gap-2">
<Input
type="number"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="font-mono"
/>
<Select value={fromUnitId} onValueChange={setFromUnitId}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{category.units.map((unit) => (
<SelectItem key={unit.id} value={unit.id}>
{unit.symbol}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Swap Button */}
<div className="flex justify-center">
<Button variant="ghost" size="icon" onClick={handleSwapUnits} className="h-8 w-8">
<ArrowLeftRight className="h-4 w-4" />
</Button>
</div>
{/* To */}
<div className="space-y-2">
<Label className="text-sm">To</Label>
<div className="flex gap-2">
<div className="flex-1 flex items-center h-9 px-3 border rounded-md bg-muted/50 font-mono tabular-nums">
{result || "—"}
</div>
<Select value={toUnitId} onValueChange={setToUnitId}>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{category.units.map((unit) => (
<SelectItem key={unit.id} value={unit.id}>
{unit.symbol}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Result Summary */}
{result && (
<p className="text-center text-sm text-muted-foreground">
{inputValue} {fromUnit.symbol} = <span className="font-medium text-foreground">{result} {toUnit.symbol}</span>
</p>
)}
</div>
</DialogContent>
</Dialog>
);
}