Skip to content

Commit 19ebf48

Browse files
committed
add star card ui
1 parent c6cc93c commit 19ebf48

File tree

12 files changed

+525
-34
lines changed

12 files changed

+525
-34
lines changed
2.11 MB
Loading
1.92 MB
Loading
1.17 MB
Loading

app/features/receive/receive-cashu-token.tsx

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ import {
2828
LinkWithViewTransition,
2929
useNavigateWithViewTransition,
3030
} from '~/lib/transitions';
31+
import { isStarAccount } from '../accounts/account';
3132
import { AccountSelector } from '../accounts/account-selector';
3233
import { tokenToMoney } from '../shared/cashu';
3334
import { getErrorMessage } from '../shared/error';
3435
import { MoneyWithConvertedAmount } from '../shared/money-with-converted-amount';
36+
import { WalletCard } from '../stars/wallet-card';
3537
import { useAuthActions } from '../user/auth';
3638
import { useFailCashuReceiveQuote } from './cashu-receive-quote-hooks';
3739
import { useCreateCashuTokenSwap } from './cashu-token-swap-hooks';
@@ -204,16 +206,22 @@ export default function ReceiveToken({
204206

205207
<div className="absolute top-0 right-0 bottom-0 left-0 mx-auto flex max-w-sm items-center justify-center">
206208
{claimableToken && receiveAccount ? (
207-
<div className="w-full max-w-sm px-4">
208-
<AccountSelector
209-
accounts={selectableAccounts}
210-
selectedAccount={receiveAccount}
211-
disabled={
212-
isCrossMintSwapDisabled || selectableAccounts.length === 1
213-
}
214-
onSelect={setReceiveAccount}
215-
/>
216-
</div>
209+
isStarAccount(receiveAccount) ? (
210+
<div className="w-full max-w-sm px-4">
211+
<WalletCard account={receiveAccount} />
212+
</div>
213+
) : (
214+
<div className="w-full max-w-sm px-4">
215+
<AccountSelector
216+
accounts={selectableAccounts}
217+
selectedAccount={receiveAccount}
218+
disabled={
219+
isCrossMintSwapDisabled || selectableAccounts.length === 1
220+
}
221+
onSelect={setReceiveAccount}
222+
/>
223+
</div>
224+
)
217225
) : (
218226
<TokenErrorDisplay
219227
message={
@@ -332,13 +340,19 @@ export function PublicReceiveCashuToken({ token }: { token: Token }) {
332340

333341
<div className="absolute top-0 right-0 bottom-0 left-0 mx-auto flex max-w-sm items-center justify-center">
334342
{claimableToken ? (
335-
<div className="w-full max-w-sm px-4">
336-
<AccountSelector
337-
accounts={[]}
338-
selectedAccount={sourceAccount}
339-
disabled={true}
340-
/>
341-
</div>
343+
isStarAccount(sourceAccount) ? (
344+
<div className="w-full max-w-sm px-4">
345+
<WalletCard account={sourceAccount} />
346+
</div>
347+
) : (
348+
<div className="w-full max-w-sm px-4">
349+
<AccountSelector
350+
accounts={[]}
351+
selectedAccount={sourceAccount}
352+
disabled={true}
353+
/>
354+
</div>
355+
)
342356
) : (
343357
<TokenErrorDisplay message={cannotClaimReason} />
344358
)}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Duration constants (in ms)
2+
export const ANIMATION_DURATION = 400;
3+
export const DETAIL_VIEW_DELAY = 300; // Delay before detail content starts animating
4+
export const OPACITY_ANIMATION_RATIO = 0.5; // Multiplier for opacity animation duration
5+
6+
export const EASE_IN_OUT = 'cubic-bezier(0.25, 0.1, 0.25, 1)';
7+
export const EASE_OUT = 'cubic-bezier(0, 0, 0.2, 1)'; // Decelerating
8+
9+
// Layout constants (in px)
10+
export const CARD_STACK_OFFSET = 64; // Space between cards in collapsed stack
11+
export const CARD_ASPECT_RATIO = 1.586; // Credit card aspect ratio
12+
13+
/**
14+
* Get the off-screen Y offset for sliding cards out
15+
*/
16+
export function getOffScreenOffset(): number {
17+
return typeof window !== 'undefined' ? window.innerHeight + 100 : 900; // Fallback for SSR
18+
}

app/features/stars/card-stack.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { CashuAccount } from '~/features/accounts/account';
2+
import {
3+
ANIMATION_DURATION,
4+
CARD_ASPECT_RATIO,
5+
CARD_STACK_OFFSET,
6+
EASE_IN_OUT,
7+
} from './animation-constants';
8+
import { SelectableWalletCard } from './wallet-card';
9+
10+
interface CardStackProps {
11+
accounts: CashuAccount[];
12+
selectedCardIndex: number | null;
13+
onCardSelect: (accountId: string, event?: React.MouseEvent) => void;
14+
}
15+
16+
/**
17+
* Displays a stack of wallet cards
18+
*/
19+
export function CardStack({
20+
accounts,
21+
selectedCardIndex,
22+
onCardSelect,
23+
}: CardStackProps) {
24+
const hasSelection = selectedCardIndex !== null;
25+
26+
return (
27+
<div className="relative w-full">
28+
{/* Spacer element that determines container height based on card stack */}
29+
<div
30+
className="pointer-events-none w-full"
31+
style={{
32+
aspectRatio: CARD_ASPECT_RATIO.toString(),
33+
marginBottom: hasSelection
34+
? 0
35+
: `${(accounts.length - 1) * CARD_STACK_OFFSET}px`,
36+
transition: `margin-bottom ${ANIMATION_DURATION}ms ${EASE_IN_OUT}`,
37+
}}
38+
/>
39+
40+
{accounts.map((account, index) => (
41+
<SelectableWalletCard
42+
key={account.id}
43+
account={account}
44+
isSelected={index === selectedCardIndex}
45+
index={index}
46+
onSelect={onCardSelect}
47+
selectedCardIndex={selectedCardIndex}
48+
/>
49+
))}
50+
</div>
51+
);
52+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Button } from '~/components/ui/button';
2+
import {
3+
type CashuAccount,
4+
getAccountBalance,
5+
} from '~/features/accounts/account';
6+
import { TransactionList } from '~/features/transactions/transaction-list';
7+
import { LinkWithViewTransition } from '~/lib/transitions';
8+
import { cn } from '~/lib/utils';
9+
import { MoneyWithConvertedAmount } from '../shared/money-with-converted-amount';
10+
import {
11+
ANIMATION_DURATION,
12+
DETAIL_VIEW_DELAY,
13+
EASE_OUT,
14+
OPACITY_ANIMATION_RATIO,
15+
} from './animation-constants';
16+
17+
interface SelectedCardDetailsProps {
18+
account: CashuAccount;
19+
isVisible: boolean;
20+
}
21+
22+
/**
23+
* Displays send/receive buttons and transaction history for the selected card.
24+
* This component shows the interactive elements below the card stack.
25+
*/
26+
export function SelectedCardDetails({
27+
account,
28+
isVisible,
29+
}: SelectedCardDetailsProps) {
30+
const balance = getAccountBalance(account);
31+
32+
const transitionStyle = `opacity ${ANIMATION_DURATION * OPACITY_ANIMATION_RATIO}ms ${EASE_OUT} ${DETAIL_VIEW_DELAY}ms`;
33+
34+
return (
35+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
36+
{/* Balance and Actions Section */}
37+
<div className="flex-shrink-0 px-6 pt-8 md:pt-12">
38+
<div
39+
className={cn(
40+
'flex flex-col items-center gap-6',
41+
isVisible ? 'opacity-100' : 'opacity-0',
42+
)}
43+
style={{ transition: transitionStyle }}
44+
>
45+
<MoneyWithConvertedAmount
46+
money={balance}
47+
otherCurrency={account.currency === 'BTC' ? 'USD' : 'BTC'}
48+
/>
49+
50+
{/* Send and Receive Buttons */}
51+
<div className="grid w-full grid-cols-2 gap-10 pt-3">
52+
<LinkWithViewTransition
53+
to={{
54+
pathname: '/receive',
55+
search: `accountId=${account.id}`,
56+
}}
57+
transition="slideUp"
58+
applyTo="newView"
59+
>
60+
<Button className="w-full py-6 text-lg">Add</Button>
61+
</LinkWithViewTransition>
62+
<LinkWithViewTransition
63+
to={{
64+
pathname: '/send',
65+
search: `accountId=${account.id}`,
66+
}}
67+
transition="slideUp"
68+
applyTo="newView"
69+
>
70+
<Button className="w-full py-6 text-lg">Send</Button>
71+
</LinkWithViewTransition>
72+
</div>
73+
</div>
74+
</div>
75+
76+
{/* Transaction List Section */}
77+
<div
78+
className={cn(
79+
'mx-auto min-h-0 w-full max-w-sm flex-1 overflow-y-auto px-6 pt-2 pb-6',
80+
isVisible ? 'opacity-100' : 'opacity-0',
81+
)}
82+
style={{ transition: transitionStyle }}
83+
>
84+
<TransactionList accountId={account.id} />
85+
</div>
86+
</div>
87+
);
88+
}

0 commit comments

Comments
 (0)