176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
"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>
|
|
);
|
|
}
|