refactor(converter): split converter card into modular section components

This commit is contained in:
2026-03-30 19:26:18 +02:00
parent a45393ac00
commit 24f21d25bc
11 changed files with 1075 additions and 755 deletions

View File

@@ -0,0 +1,175 @@
"use client";
import { motion } from "framer-motion";
import { MoveRight, Star, X } from "lucide-react";
import { CurrencyIcon } from "@/components/converter/currency-icon";
import { Button } from "@/components/ui/button";
import { type PinnedPair } from "@/hooks/use-pinned-pairs";
import type { RateAsset } from "@/lib/rates";
import { cn } from "@/lib/utils";
interface PinnedPairItem {
pair: PinnedPair;
fromAsset: RateAsset;
toAsset: RateAsset;
}
interface ConverterCardPinnedPairsProps {
shouldReduceMotion: boolean;
isCurrentPairPinned: boolean;
hasReachedPinnedPairsLimit: boolean;
pinStarAnimationStep: number;
pinnedPairItems: PinnedPairItem[];
fromCode: string;
toCode: string;
isPinnedPairsReady: boolean;
maxPinnedPairs: number;
onTogglePinnedCurrentPair: () => void;
onSelectPinnedPair: (pair: PinnedPair) => void;
onRemovePinnedPair: (pair: PinnedPair) => void;
}
export function ConverterCardPinnedPairs({
shouldReduceMotion,
isCurrentPairPinned,
hasReachedPinnedPairsLimit,
pinStarAnimationStep,
pinnedPairItems,
fromCode,
toCode,
isPinnedPairsReady,
maxPinnedPairs,
onTogglePinnedCurrentPair,
onSelectPinnedPair,
onRemovePinnedPair,
}: ConverterCardPinnedPairsProps) {
return (
<div className="space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-xs uppercase tracking-[0.12em] text-muted-foreground">
Pinned pairs
</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={onTogglePinnedCurrentPair}
disabled={hasReachedPinnedPairsLimit}
aria-pressed={isCurrentPairPinned}
aria-label={
isCurrentPairPinned
? "Remove current pair from pinned pairs"
: "Add current pair to pinned pairs"
}
className={cn(
"h-8 rounded-full border-border/70 px-3 text-xs transition-colors",
isCurrentPairPinned
? "border-amber-300/50 bg-amber-400/10 text-amber-100 hover:border-amber-300/60 hover:bg-amber-400/15"
: "text-muted-foreground hover:text-foreground",
)}
>
<motion.span
key={`${pinStarAnimationStep}-${isCurrentPairPinned ? "on" : "off"}`}
className="mr-1.5 inline-flex"
initial={
shouldReduceMotion
? false
: { scale: 0.95, rotate: 0, opacity: 0.9 }
}
animate={
shouldReduceMotion
? { scale: 1, rotate: 0, opacity: 1 }
: {
scale: [1, 1.18, 1],
rotate: isCurrentPairPinned ? [0, -12, 8, 0] : [0, 12, -8, 0],
opacity: [1, 1, 1],
}
}
transition={{
duration: shouldReduceMotion ? 0.01 : 0.24,
ease: [0.22, 1, 0.36, 1],
}}
>
<Star
className={cn(
"h-3.5 w-3.5",
isCurrentPairPinned ? "fill-amber-300 text-amber-300" : "",
)}
/>
</motion.span>
{isCurrentPairPinned ? "Pinned" : "Pin current pair"}
</Button>
</div>
{pinnedPairItems.length > 0 ? (
<div className="flex flex-wrap gap-2">
{pinnedPairItems.map(({ pair, fromAsset, toAsset }) => {
const isActive = pair.fromCode === fromCode && pair.toCode === toCode;
return (
<div
key={`${pair.fromCode}-${pair.toCode}`}
className={cn(
"inline-flex items-center rounded-full border border-border/70 bg-background/45 pr-0.5",
isActive ? "border-cyan-300/40 bg-cyan-500/10" : "",
)}
>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onSelectPinnedPair(pair)}
className={cn(
"h-8 rounded-full px-2.5 text-xs text-muted-foreground hover:text-foreground",
isActive ? "text-cyan-100 hover:text-cyan-100" : "",
)}
aria-label={`Set pair ${pair.fromCode} to ${pair.toCode}`}
>
<span className="inline-flex items-center gap-1.5">
<CurrencyIcon
code={fromAsset.code}
type={fromAsset.type}
size="sm"
/>
<span>{pair.fromCode}</span>
<MoveRight className="h-3.5 w-3.5 text-cyan-300/90" />
<CurrencyIcon
code={toAsset.code}
type={toAsset.type}
size="sm"
/>
<span>{pair.toCode}</span>
</span>
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => onRemovePinnedPair(pair)}
className="h-7 w-7 rounded-full text-muted-foreground hover:text-red-300 focus-visible:text-red-200"
aria-label={`Remove pinned pair ${pair.fromCode} to ${pair.toCode}`}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
);
})}
</div>
) : null}
{isPinnedPairsReady && pinnedPairItems.length === 0 ? (
<p className="text-xs text-muted-foreground">
Pin your most-used pairs for one-click access.
</p>
) : null}
{hasReachedPinnedPairsLimit ? (
<p className="text-xs text-amber-200/90">
Pinned pairs limit reached ({maxPinnedPairs}). Remove one to add a new
pair.
</p>
) : null}
</div>
);
}