refactor(converter): split converter card into modular section components
This commit is contained in:
175
components/converter/converter-card-pinned-pairs.tsx
Normal file
175
components/converter/converter-card-pinned-pairs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user