improve landing UI and open-source branding
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
formatAmount,
|
formatAmount,
|
||||||
formatInverseRate,
|
formatInverseRate,
|
||||||
formatRate,
|
formatRate,
|
||||||
formatTimestamp
|
formatTimestamp,
|
||||||
} from "@/lib/format";
|
} from "@/lib/format";
|
||||||
import { buildRateMap, convertAmount } from "@/lib/rates";
|
import { buildRateMap, convertAmount } from "@/lib/rates";
|
||||||
import { validateAmount } from "@/lib/validation";
|
import { validateAmount } from "@/lib/validation";
|
||||||
@@ -62,7 +62,7 @@ function ConverterSkeleton() {
|
|||||||
|
|
||||||
function ErrorState({
|
function ErrorState({
|
||||||
message,
|
message,
|
||||||
onRetry
|
onRetry,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
@@ -73,11 +73,17 @@ function ErrorState({
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertTriangle className="mt-0.5 h-5 w-5 text-red-300" />
|
<AlertTriangle className="mt-0.5 h-5 w-5 text-red-300" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-red-200">Unable to load market rates</p>
|
<p className="text-sm font-medium text-red-200">
|
||||||
|
Unable to load market rates
|
||||||
|
</p>
|
||||||
<p className="mt-1 text-sm text-red-200/80">{message}</p>
|
<p className="mt-1 text-sm text-red-200/80">{message}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={onRetry} className="w-fit border-red-300/30">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onRetry}
|
||||||
|
className="w-fit border-red-300/30"
|
||||||
|
>
|
||||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
@@ -101,7 +107,7 @@ function EmptyState() {
|
|||||||
export function ConverterCard({
|
export function ConverterCard({
|
||||||
forcedFromCode,
|
forcedFromCode,
|
||||||
forcedToCode,
|
forcedToCode,
|
||||||
onPairChange
|
onPairChange,
|
||||||
}: ConverterCardProps) {
|
}: ConverterCardProps) {
|
||||||
const { data, error, isLoading, refresh } = useMarketRates();
|
const { data, error, isLoading, refresh } = useMarketRates();
|
||||||
|
|
||||||
@@ -126,7 +132,8 @@ export function ConverterCard({
|
|||||||
if (!rateMap.has(toCode)) {
|
if (!rateMap.has(toCode)) {
|
||||||
const fallback = rateMap.has(DEFAULT_TO)
|
const fallback = rateMap.has(DEFAULT_TO)
|
||||||
? DEFAULT_TO
|
? DEFAULT_TO
|
||||||
: assets.find((asset) => asset.code !== fromCode)?.code ?? assets[0].code;
|
: (assets.find((asset) => asset.code !== fromCode)?.code ??
|
||||||
|
assets[0].code);
|
||||||
|
|
||||||
setToCode(fallback);
|
setToCode(fallback);
|
||||||
}
|
}
|
||||||
@@ -209,26 +216,35 @@ export function ConverterCard({
|
|||||||
<CardHeader className="relative z-10 rounded-t-2xl bg-gradient-to-r from-sky-500/10 via-cyan-400/5 to-emerald-500/10 pb-4 sm:pb-5">
|
<CardHeader className="relative z-10 rounded-t-2xl bg-gradient-to-r from-sky-500/10 via-cyan-400/5 to-emerald-500/10 pb-4 sm:pb-5">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<CardTitle className="text-2xl font-semibold tracking-tight">
|
<CardTitle className="text-2xl font-semibold tracking-tight">
|
||||||
Smart Converter
|
Currency Converter
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="border-border/70 bg-background/50">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-border/70 bg-background/50"
|
||||||
|
>
|
||||||
Fiat: {data.sources.fiat}
|
Fiat: {data.sources.fiat}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="border-border/70 bg-background/50">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-border/70 bg-background/50"
|
||||||
|
>
|
||||||
Crypto: {data.sources.crypto}
|
Crypto: {data.sources.crypto}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription className="pr-1 text-base/7 sm:text-sm">
|
<CardDescription className="pr-1 text-base/7 sm:text-sm">
|
||||||
Convert fiat and crypto assets instantly using live normalized USD quote data.
|
Convert fiat currencies and cryptocurrencies using live exchange
|
||||||
|
rates.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="relative z-10 space-y-5 pt-4 sm:pt-5">
|
<CardContent className="relative z-10 space-y-5 pt-4 sm:pt-5">
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="flex items-center gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
|
<div className="flex items-center gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<span>Using last successful data. Latest refresh failed: {error}</span>
|
<span>
|
||||||
|
Using last successful data. Latest refresh failed: {error}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -289,12 +305,20 @@ export function ConverterCard({
|
|||||||
{fromAsset && toAsset ? (
|
{fromAsset && toAsset ? (
|
||||||
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-border/70 bg-background/60 px-2 py-1">
|
<span className="inline-flex items-center gap-1.5 rounded-full border border-border/70 bg-background/60 px-2 py-1">
|
||||||
<CurrencyIcon code={fromAsset.code} type={fromAsset.type} size="sm" />
|
<CurrencyIcon
|
||||||
|
code={fromAsset.code}
|
||||||
|
type={fromAsset.type}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
{fromAsset.code}
|
{fromAsset.code}
|
||||||
</span>
|
</span>
|
||||||
<span>to</span>
|
<span>to</span>
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-border/70 bg-background/60 px-2 py-1">
|
<span className="inline-flex items-center gap-1.5 rounded-full border border-border/70 bg-background/60 px-2 py-1">
|
||||||
<CurrencyIcon code={toAsset.code} type={toAsset.type} size="sm" />
|
<CurrencyIcon
|
||||||
|
code={toAsset.code}
|
||||||
|
type={toAsset.type}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
{toAsset.code}
|
{toAsset.code}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,7 +328,10 @@ export function ConverterCard({
|
|||||||
</p>
|
</p>
|
||||||
{fromAsset ? (
|
{fromAsset ? (
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
for {inputValidation.ok ? formatAmount(inputValidation.value, fromAsset) : "-"}{" "}
|
for{" "}
|
||||||
|
{inputValidation.ok
|
||||||
|
? formatAmount(inputValidation.value, fromAsset)
|
||||||
|
: "-"}{" "}
|
||||||
{fromAsset.code}
|
{fromAsset.code}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Github } from "lucide-react";
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
return (
|
return (
|
||||||
<section className="relative pt-10 sm:pt-14">
|
<section className="relative pt-10 sm:pt-14">
|
||||||
<div className="mx-auto max-w-3xl text-center">
|
<div className="mx-auto max-w-3xl text-center">
|
||||||
<Badge
|
<div className="mb-4 flex justify-center">
|
||||||
variant="outline"
|
<div className="inline-flex items-center gap-2 rounded-full border border-border/70 bg-background/60 px-3 py-1 text-xs text-muted-foreground backdrop-blur">
|
||||||
className="border-cyan-400/40 bg-cyan-400/10 px-3 py-1 text-cyan-200"
|
<span className="text-foreground">Open Source</span>
|
||||||
|
<span className="text-muted-foreground/70">•</span>
|
||||||
|
<span className="text-foreground">MIT License</span>
|
||||||
|
<span className="text-muted-foreground/70">•</span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/zvspany/nexcurrency"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
aria-label="View on GitHub"
|
||||||
|
title="View on GitHub"
|
||||||
|
className="inline-flex items-center text-cyan-200 transition-colors hover:text-cyan-100"
|
||||||
>
|
>
|
||||||
Real-time fiat and crypto conversion
|
<Github className="h-4 w-4" />
|
||||||
</Badge>
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h1 className="mt-5 text-balance font-heading text-4xl font-semibold leading-tight tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
<h1 className="mt-5 text-balance font-heading text-4xl font-semibold leading-tight tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
||||||
Convert Global Currencies and Crypto in One Premium Workspace
|
Open-source fiat and crypto converter
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto mt-4 max-w-2xl text-balance text-base text-muted-foreground sm:text-lg">
|
<p className="mx-auto mt-4 max-w-2xl text-balance text-base text-muted-foreground sm:text-lg">
|
||||||
Instantly switch between fiat and digital assets with live rates, smart
|
Convert fiat currencies and cryptocurrencies using live exchange
|
||||||
formatting, and a clean professional interface built for speed.
|
rates.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ArrowRightLeft, Landmark, Layers, MoveRight, Workflow } from "lucide-react";
|
import {
|
||||||
|
ArrowRightLeft,
|
||||||
|
Landmark,
|
||||||
|
Layers,
|
||||||
|
MoveRight,
|
||||||
|
Workflow,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import { CurrencyIcon } from "@/components/converter/currency-icon";
|
import { CurrencyIcon } from "@/components/converter/currency-icon";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -23,31 +29,31 @@ const popularPairs: PopularPair[] = [
|
|||||||
{ from: "ETH", to: "SOL", fromType: "crypto", toType: "crypto" },
|
{ from: "ETH", to: "SOL", fromType: "crypto", toType: "crypto" },
|
||||||
{ from: "XMR", to: "BTC", fromType: "crypto", toType: "crypto" },
|
{ from: "XMR", to: "BTC", fromType: "crypto", toType: "crypto" },
|
||||||
{ from: "LTC", to: "BTC", fromType: "crypto", toType: "crypto" },
|
{ from: "LTC", to: "BTC", fromType: "crypto", toType: "crypto" },
|
||||||
{ from: "CHF", to: "JPY", fromType: "fiat", toType: "fiat" }
|
{ from: "CHF", to: "JPY", fromType: "fiat", toType: "fiat" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const howItWorks = [
|
const howItWorks = [
|
||||||
"Fiat rates are fetched from Frankfurter with USD as the common quote.",
|
"Fiat rates are fetched from Frankfurter with USD as the common quote.",
|
||||||
"Crypto prices are fetched from CoinGecko in USD.",
|
"Crypto prices are fetched from CoinGecko in USD.",
|
||||||
"Any pair is converted through normalized USD-per-unit pricing."
|
"Any pair is converted through normalized USD-per-unit pricing.",
|
||||||
];
|
];
|
||||||
|
|
||||||
const supportedAssets = [
|
const supportedAssets = [
|
||||||
{
|
{
|
||||||
title: "Fiat currencies",
|
title: "Fiat currencies",
|
||||||
description:
|
description:
|
||||||
"Supports a broad list of global government-issued currencies, including major and regional pairs."
|
"Supports a broad list of global government-issued currencies, including major and regional pairs.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Cryptocurrencies",
|
title: "Cryptocurrencies",
|
||||||
description:
|
description:
|
||||||
"Includes leading crypto assets such as BTC, ETH, LTC, XMR, SOL, USDT, and more."
|
"Includes leading crypto assets such as BTC, ETH, LTC, XMR, SOL, USDT, and more.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Cross-market pairs",
|
title: "Cross-market pairs",
|
||||||
description:
|
description:
|
||||||
"Convert fiat-to-crypto, crypto-to-fiat, and crypto-to-crypto in one unified experience."
|
"Convert fiat-to-crypto, crypto-to-fiat, and crypto-to-crypto in one unified experience.",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface InsightsSectionProps {
|
interface InsightsSectionProps {
|
||||||
@@ -59,7 +65,7 @@ interface InsightsSectionProps {
|
|||||||
export function InsightsSection({
|
export function InsightsSection({
|
||||||
selectedFromCode,
|
selectedFromCode,
|
||||||
selectedToCode,
|
selectedToCode,
|
||||||
onSelectPopularPair
|
onSelectPopularPair,
|
||||||
}: InsightsSectionProps) {
|
}: InsightsSectionProps) {
|
||||||
return (
|
return (
|
||||||
<section className="mt-10 grid gap-5 lg:grid-cols-3">
|
<section className="mt-10 grid gap-5 lg:grid-cols-3">
|
||||||
@@ -67,7 +73,7 @@ export function InsightsSection({
|
|||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<ArrowRightLeft className="h-4 w-4 text-sky-300" />
|
<ArrowRightLeft className="h-4 w-4 text-sky-300" />
|
||||||
Popular conversions
|
Common pairs
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-wrap gap-2 pt-0">
|
<CardContent className="flex flex-wrap gap-2 pt-0">
|
||||||
@@ -86,11 +92,15 @@ export function InsightsSection({
|
|||||||
"h-9 rounded-full border-border/70 bg-background/50 px-3.5 font-normal transition-all",
|
"h-9 rounded-full border-border/70 bg-background/50 px-3.5 font-normal transition-all",
|
||||||
"hover:border-cyan-400/40 hover:bg-cyan-400/10 hover:text-cyan-100",
|
"hover:border-cyan-400/40 hover:bg-cyan-400/10 hover:text-cyan-100",
|
||||||
isActive &&
|
isActive &&
|
||||||
"border-cyan-400/50 bg-cyan-400/15 text-cyan-100 shadow-[0_0_0_1px_hsl(189_100%_40%_/_0.2)]"
|
"border-cyan-400/50 bg-cyan-400/15 text-cyan-100 shadow-[0_0_0_1px_hsl(189_100%_40%_/_0.2)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-1.5 text-xs sm:text-sm">
|
<span className="inline-flex items-center gap-1.5 text-xs sm:text-sm">
|
||||||
<CurrencyIcon code={pair.from} type={pair.fromType} size="sm" />
|
<CurrencyIcon
|
||||||
|
code={pair.from}
|
||||||
|
type={pair.fromType}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
<span>{pair.from}</span>
|
<span>{pair.from}</span>
|
||||||
<MoveRight className="h-3.5 w-3.5 text-cyan-300" />
|
<MoveRight className="h-3.5 w-3.5 text-cyan-300" />
|
||||||
<CurrencyIcon code={pair.to} type={pair.toType} size="sm" />
|
<CurrencyIcon code={pair.to} type={pair.toType} size="sm" />
|
||||||
@@ -111,7 +121,10 @@ export function InsightsSection({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 pt-0 text-sm text-muted-foreground">
|
<CardContent className="space-y-3 pt-0 text-sm text-muted-foreground">
|
||||||
{howItWorks.map((line) => (
|
{howItWorks.map((line) => (
|
||||||
<p key={line} className="rounded-lg border border-border/60 bg-background/40 px-3 py-2">
|
<p
|
||||||
|
key={line}
|
||||||
|
className="rounded-lg border border-border/60 bg-background/40 px-3 py-2"
|
||||||
|
>
|
||||||
{line}
|
{line}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -122,7 +135,7 @@ export function InsightsSection({
|
|||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<Layers className="h-4 w-4 text-emerald-300" />
|
<Layers className="h-4 w-4 text-emerald-300" />
|
||||||
Supported asset types
|
Supported currencies
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 pt-0 text-sm">
|
<CardContent className="space-y-3 pt-0 text-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user