feat: add currency converter tool component
- Introduced a new Currency Converter component that allows users to select and convert between various currencies in real-time. - Implemented local storage for saving selected currencies and caching for exchange rates to enhance user experience. - Updated the tool index to include the new Currency Converter with an appropriate icon and description.
This commit is contained in:
451
src/components/tool/currency.tsx
Normal file
451
src/components/tool/currency.tsx
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
import { useState, useEffect, type FC } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Check, X, Plus } from "lucide-react";
|
||||||
|
|
||||||
|
interface Currency {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 常用货币列表 (仅包含 Frankfurter API 支持的货币)
|
||||||
|
const AVAILABLE_CURRENCIES: Currency[] = [
|
||||||
|
{ code: "USD", name: "美元", symbol: "$" },
|
||||||
|
{ code: "CNY", name: "人民币", symbol: "¥" },
|
||||||
|
{ code: "EUR", name: "欧元", symbol: "€" },
|
||||||
|
{ code: "GBP", name: "英镑", symbol: "£" },
|
||||||
|
{ code: "JPY", name: "日元", symbol: "¥" },
|
||||||
|
{ code: "HKD", name: "港币", symbol: "HK$" },
|
||||||
|
{ code: "AUD", name: "澳元", symbol: "A$" },
|
||||||
|
{ code: "CAD", name: "加元", symbol: "C$" },
|
||||||
|
{ code: "SGD", name: "新加坡元", symbol: "S$" },
|
||||||
|
{ code: "CHF", name: "瑞士法郎", symbol: "CHF" },
|
||||||
|
{ code: "NZD", name: "新西兰元", symbol: "NZ$" },
|
||||||
|
{ code: "KRW", name: "韩元", symbol: "₩" },
|
||||||
|
{ code: "THB", name: "泰铢", symbol: "฿" },
|
||||||
|
{ code: "MYR", name: "马来西亚林吉特", symbol: "RM" },
|
||||||
|
{ code: "INR", name: "印度卢比", symbol: "₹" },
|
||||||
|
{ code: "BRL", name: "巴西雷亚尔", symbol: "R$" },
|
||||||
|
{ code: "MXN", name: "墨西哥比索", symbol: "MX$" },
|
||||||
|
{ code: "SEK", name: "瑞典克朗", symbol: "kr" },
|
||||||
|
{ code: "NOK", name: "挪威克朗", symbol: "kr" },
|
||||||
|
{ code: "DKK", name: "丹麦克朗", symbol: "kr" },
|
||||||
|
{ code: "PLN", name: "波兰兹罗提", symbol: "zł" },
|
||||||
|
{ code: "TRY", name: "土耳其里拉", symbol: "₺" },
|
||||||
|
{ code: "PHP", name: "菲律宾比索", symbol: "₱" },
|
||||||
|
{ code: "IDR", name: "印尼盾", symbol: "Rp" },
|
||||||
|
{ code: "ILS", name: "以色列新谢克尔", symbol: "₪" },
|
||||||
|
{ code: "CZK", name: "捷克克朗", symbol: "Kč" },
|
||||||
|
{ code: "RON", name: "罗马尼亚列伊", symbol: "lei" },
|
||||||
|
{ code: "HUF", name: "匈牙利福林", symbol: "Ft" },
|
||||||
|
{ code: "BGN", name: "保加利亚列弗", symbol: "лв" },
|
||||||
|
{ code: "ISK", name: "冰岛克朗", symbol: "kr" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 必选货币(不可删除) - 美元第一,人民币第二
|
||||||
|
const REQUIRED_CURRENCIES = ["USD", "CNY"];
|
||||||
|
|
||||||
|
const STORAGE_KEY = "selectedCurrencies";
|
||||||
|
const RATES_CACHE_KEY = "currencyRatesCache";
|
||||||
|
const RATES_CACHE_DURATION = 60 * 60 * 1000; // 1小时(毫秒)
|
||||||
|
|
||||||
|
const Tool: FC = () => {
|
||||||
|
const [amounts, setAmounts] = useState<Record<string, string>>({});
|
||||||
|
const [selectedCurrencies, setSelectedCurrencies] = useState<string[]>([]);
|
||||||
|
const [rates, setRates] = useState<Record<string, number>>({});
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// 从 localStorage 加载已选货币
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
if (Array.isArray(parsed) && parsed.length >= 2) {
|
||||||
|
setSelectedCurrencies(parsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load saved currencies:", error);
|
||||||
|
}
|
||||||
|
// 默认选择 USD 和 CNY (美元第一)
|
||||||
|
setSelectedCurrencies(["USD", "CNY"]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 保存已选货币到 localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCurrencies.length >= 2) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(selectedCurrencies));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save currencies:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedCurrencies]);
|
||||||
|
|
||||||
|
// 获取汇率数据 - 使用 stale-while-revalidate 缓存策略
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRates = async (useCache = true) => {
|
||||||
|
try {
|
||||||
|
// 1. 尝试从缓存加载
|
||||||
|
if (useCache) {
|
||||||
|
const cached = localStorage.getItem(RATES_CACHE_KEY);
|
||||||
|
if (cached) {
|
||||||
|
try {
|
||||||
|
const { rates: cachedRates, timestamp } = JSON.parse(cached);
|
||||||
|
const age = Date.now() - timestamp;
|
||||||
|
|
||||||
|
if (age < RATES_CACHE_DURATION) {
|
||||||
|
// 缓存未过期,直接使用
|
||||||
|
setRates(cachedRates);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 缓存过期,先使用旧数据,后台更新
|
||||||
|
setRates(cachedRates);
|
||||||
|
setLoading(false);
|
||||||
|
// 继续执行下面的网络请求
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse cached rates:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从网络获取最新数据
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("https://api.frankfurter.app/latest?base=USD", {
|
||||||
|
cache: "no-cache", // 绕过浏览器缓存,确保获取最新数据
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const allRates: Record<string, number> = { USD: 1, ...data.rates };
|
||||||
|
|
||||||
|
// 3. 更新状态和缓存
|
||||||
|
setRates(allRates);
|
||||||
|
localStorage.setItem(
|
||||||
|
RATES_CACHE_KEY,
|
||||||
|
JSON.stringify({ rates: allRates, timestamp: Date.now() })
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// 如果有缓存,即使网络请求失败也继续使用缓存
|
||||||
|
const cached = localStorage.getItem(RATES_CACHE_KEY);
|
||||||
|
if (cached) {
|
||||||
|
try {
|
||||||
|
const { rates: cachedRates } = JSON.parse(cached);
|
||||||
|
setRates(cachedRates);
|
||||||
|
toast.info("使用缓存的汇率数据(网络请求失败)");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to use cached rates:", e);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(`获取汇率失败: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
toast.error("获取汇率失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(`获取汇率失败: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
toast.error("获取汇率失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchRates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 初始化金额并在汇率加载后计算其他货币
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCurrencies.length > 0 && Object.keys(rates).length > 0) {
|
||||||
|
if (Object.keys(amounts).length === 0) {
|
||||||
|
// 首次初始化,以 1 USD 为基准
|
||||||
|
const initialAmounts: Record<string, string> = {};
|
||||||
|
const baseAmountInUSD = 1; // 1美元
|
||||||
|
|
||||||
|
selectedCurrencies.forEach(code => {
|
||||||
|
if (rates[code]) {
|
||||||
|
const convertedAmount = baseAmountInUSD * rates[code];
|
||||||
|
initialAmounts[code] = convertedAmount.toFixed(2);
|
||||||
|
} else {
|
||||||
|
initialAmounts[code] = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setAmounts(initialAmounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedCurrencies, rates, amounts]);
|
||||||
|
|
||||||
|
// 监听汇率更新,自动重新计算所有金额
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(rates).length > 0 && Object.keys(amounts).length > 0) {
|
||||||
|
// 找到第一个有值的货币作为基准
|
||||||
|
const baseCurrency = selectedCurrencies.find(code => amounts[code] && parseFloat(amounts[code]) > 0);
|
||||||
|
|
||||||
|
if (baseCurrency && rates[baseCurrency]) {
|
||||||
|
const baseValue = parseFloat(amounts[baseCurrency]);
|
||||||
|
const amountInUSD = baseValue / rates[baseCurrency];
|
||||||
|
|
||||||
|
// 重新计算所有货币
|
||||||
|
const updatedAmounts: Record<string, string> = {};
|
||||||
|
selectedCurrencies.forEach(code => {
|
||||||
|
if (rates[code]) {
|
||||||
|
const convertedAmount = amountInUSD * rates[code];
|
||||||
|
updatedAmounts[code] = convertedAmount.toFixed(2);
|
||||||
|
} else {
|
||||||
|
updatedAmounts[code] = amounts[code] || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只在值有变化时更新
|
||||||
|
const hasChanged = selectedCurrencies.some(
|
||||||
|
code => updatedAmounts[code] !== amounts[code]
|
||||||
|
);
|
||||||
|
if (hasChanged) {
|
||||||
|
setAmounts(updatedAmounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [rates]);
|
||||||
|
|
||||||
|
// 处理货币金额变化
|
||||||
|
const handleAmountChange = (currencyCode: string, value: string) => {
|
||||||
|
// 只允许数字和小数点
|
||||||
|
if (value !== "" && !/^\d*\.?\d*$/.test(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rates[currencyCode]) {
|
||||||
|
setAmounts({ ...amounts, [currencyCode]: value });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果输入为空,清空所有货币
|
||||||
|
if (value === "" || value === "0" || parseFloat(value) === 0) {
|
||||||
|
const newAmounts: Record<string, string> = {};
|
||||||
|
selectedCurrencies.forEach(code => {
|
||||||
|
newAmounts[code] = "";
|
||||||
|
});
|
||||||
|
setAmounts(newAmounts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputValue = parseFloat(value);
|
||||||
|
|
||||||
|
// 计算其他货币的值
|
||||||
|
const newAmounts: Record<string, string> = { [currencyCode]: value };
|
||||||
|
|
||||||
|
// 先转换为 USD
|
||||||
|
const amountInUSD = inputValue / rates[currencyCode];
|
||||||
|
|
||||||
|
// 转换为其他货币
|
||||||
|
selectedCurrencies.forEach(code => {
|
||||||
|
if (code !== currencyCode && rates[code]) {
|
||||||
|
const convertedAmount = amountInUSD * rates[code];
|
||||||
|
newAmounts[code] = convertedAmount.toFixed(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setAmounts(newAmounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加货币
|
||||||
|
const addCurrency = (currencyCode: string) => {
|
||||||
|
if (!selectedCurrencies.includes(currencyCode)) {
|
||||||
|
setSelectedCurrencies([...selectedCurrencies, currencyCode]);
|
||||||
|
|
||||||
|
// 计算新货币的初始金额
|
||||||
|
if (rates[currencyCode] && Object.keys(amounts).length > 0) {
|
||||||
|
const firstCurrency = selectedCurrencies[0];
|
||||||
|
if (firstCurrency && amounts[firstCurrency] && rates[firstCurrency]) {
|
||||||
|
const firstAmount = parseFloat(amounts[firstCurrency]) || 0;
|
||||||
|
const amountInUSD = firstAmount / rates[firstCurrency];
|
||||||
|
const newAmount = amountInUSD * rates[currencyCode];
|
||||||
|
setAmounts({ ...amounts, [currencyCode]: newAmount.toFixed(2) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(`已添加 ${currencyCode}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除货币
|
||||||
|
const removeCurrency = (currencyCode: string) => {
|
||||||
|
if (REQUIRED_CURRENCIES.includes(currencyCode)) {
|
||||||
|
toast.error(`${currencyCode} 是必选货币,无法删除`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedCurrencies.length <= 2) {
|
||||||
|
toast.error("至少需要保留两种货币");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedCurrencies(selectedCurrencies.filter((c) => c !== currencyCode));
|
||||||
|
|
||||||
|
// 删除对应的金额
|
||||||
|
const newAmounts = { ...amounts };
|
||||||
|
delete newAmounts[currencyCode];
|
||||||
|
setAmounts(newAmounts);
|
||||||
|
|
||||||
|
toast.success(`已删除 ${currencyCode}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取货币信息
|
||||||
|
const getCurrency = (code: string): Currency | undefined => {
|
||||||
|
return AVAILABLE_CURRENCIES.find((c) => c.code === code);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 h-full">
|
||||||
|
|
||||||
|
{/* 货币选择器 */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>选择货币</Label>
|
||||||
|
{/* 添加货币按钮 - 固定在右上角 */}
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="gap-1">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
添加货币
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[300px] p-0" align="end" side="bottom" sideOffset={5}>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="搜索货币..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>未找到货币</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{AVAILABLE_CURRENCIES.map((currency) => {
|
||||||
|
const isSelected = selectedCurrencies.includes(currency.code);
|
||||||
|
const isRequired = REQUIRED_CURRENCIES.includes(currency.code);
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={currency.code}
|
||||||
|
value={`${currency.code} ${currency.name}`}
|
||||||
|
disabled={isRequired}
|
||||||
|
onSelect={() => {
|
||||||
|
if (isRequired) return; // 必选货币不可操作
|
||||||
|
if (isSelected && selectedCurrencies.length > 2) {
|
||||||
|
removeCurrency(currency.code);
|
||||||
|
} else if (!isSelected) {
|
||||||
|
addCurrency(currency.code);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={`mr-2 size-4 ${
|
||||||
|
isSelected ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="flex-1">
|
||||||
|
{currency.symbol} {currency.code} - {currency.name}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 已选货币 Badges */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedCurrencies.map((code) => {
|
||||||
|
const currency = getCurrency(code);
|
||||||
|
const isRequired = REQUIRED_CURRENCIES.includes(code);
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={code}
|
||||||
|
variant={isRequired ? "default" : "secondary"}
|
||||||
|
className="gap-1 pr-1"
|
||||||
|
>
|
||||||
|
<span>{currency?.symbol} {code}</span>
|
||||||
|
{!isRequired && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 rounded-sm opacity-70 hover:opacity-100 hover:text-destructive transition-opacity"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
removeCurrency(code);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="size-3 pointer-events-auto" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{REQUIRED_CURRENCIES.map((c) => getCurrency(c)?.code).join("、")} 为必选货币,至少保留两种货币
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 货币列表 */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||||
|
加载汇率数据中...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-3 flex-1 overflow-auto">
|
||||||
|
<div className="text-sm font-medium">货币金额:</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 auto-rows-min">
|
||||||
|
{selectedCurrencies.map((code) => {
|
||||||
|
const currency = getCurrency(code);
|
||||||
|
if (!currency) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={code} className="flex flex-col">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
{currency.symbol} {currency.code} - {currency.name}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入金额"
|
||||||
|
value={amounts[code] || ""}
|
||||||
|
onChange={(e) => handleAmountChange(code, e.target.value)}
|
||||||
|
className="text-xl font-semibold"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && Object.keys(rates).length > 0 && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
汇率数据来源: Frankfurter API (基于欧洲央行数据)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tool;
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { lazy, type ReactNode, type ComponentType } from 'react';
|
import { lazy, type ReactNode, type ComponentType } from 'react';
|
||||||
import { FileJson, Hash, Binary, Network, Globe, Activity, Gauge, Wifi, MapPin } from 'lucide-react'
|
import { FileJson, Hash, Binary, Network, Globe, Activity, Gauge, Wifi, MapPin, Coins } from 'lucide-react'
|
||||||
|
|
||||||
// 懒加载工具组件
|
// 懒加载工具组件
|
||||||
const UUID = lazy(() => import('./uuid'))
|
const UUID = lazy(() => import('./uuid'))
|
||||||
const JSON = lazy(() => import('./json'))
|
const JSON = lazy(() => import('./json'))
|
||||||
const Base64 = lazy(() => import('./base64'))
|
const Base64 = lazy(() => import('./base64'))
|
||||||
|
const Currency = lazy(() => import('./currency'))
|
||||||
const DNS = lazy(() => import('./network/dns'))
|
const DNS = lazy(() => import('./network/dns'))
|
||||||
const Ping = lazy(() => import('./network/ping'))
|
const Ping = lazy(() => import('./network/ping'))
|
||||||
const TCPing = lazy(() => import('./network/tcping'))
|
const TCPing = lazy(() => import('./network/tcping'))
|
||||||
@@ -42,6 +43,13 @@ export const tools: Tool[] = [
|
|||||||
icon: <Binary />,
|
icon: <Binary />,
|
||||||
component: Base64,
|
component: Base64,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "currency",
|
||||||
|
name: "Currency Converter",
|
||||||
|
description: "Real-time currency exchange rates",
|
||||||
|
icon: <Coins />,
|
||||||
|
component: Currency,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "network",
|
path: "network",
|
||||||
name: "Network Tools",
|
name: "Network Tools",
|
||||||
|
|||||||
Reference in New Issue
Block a user