improve landing UI and open-source branding

This commit is contained in:
zvspany
2026-03-08 12:31:00 +01:00
parent 962081d48b
commit a76cd67581
3 changed files with 90 additions and 38 deletions

View File

@@ -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}

View File

@@ -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>
Real-time fiat and crypto conversion <span className="text-foreground">MIT License</span>
</Badge> <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"
>
<Github className="h-4 w-4" />
</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>

View File

@@ -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">