Skip to content

Commit 1620a87

Browse files
feat(wallet/frontend): separate card state from side view (#1555)
* Add initial cards api * Add card service mock * Format * Separate card state from view; use context * Format * Update card actions; layout shift fixed * Center card on mobile * Fix overflowing for text * Consistent naming across components
1 parent f914cce commit 1620a87

4 files changed

Lines changed: 208 additions & 96 deletions

File tree

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
1-
import type { ComponentProps } from 'react'
1+
import { useState, type ComponentProps } from 'react'
22
import { CopyButton } from '@/ui/CopyButton'
33
import { Chip, GateHubLogo, MasterCardLogo } from '../icons/UserCardIcons'
44
import { cn } from '@/utils/helpers'
5-
6-
const CARD_TYPE = {
7-
normal: 'normal',
8-
details: 'details',
9-
frozen: 'frozen'
10-
} as const
11-
12-
export type CardType = keyof typeof CARD_TYPE
13-
14-
interface UserCardProps {
15-
type: CardType
16-
name: string
17-
}
5+
import type { IUserCard } from '@/lib/api/card'
6+
import { useCardContext, UserCardContext } from './UserCardContext'
7+
import { UserCardActions } from './UserCardActions'
188

199
export type UserCardContainerProps = ComponentProps<'div'>
2010

@@ -36,18 +26,14 @@ const UserCardContainer = ({
3626
)
3727
}
3828

39-
interface UserCardFrontProps {
40-
name: UserCardProps['name']
41-
isFrozen: boolean
42-
}
43-
44-
const UserCardFront = ({ name, isFrozen }: UserCardFrontProps) => {
29+
const UserCardFront = () => {
30+
const { card } = useCardContext()
4531
return (
4632
<UserCardContainer>
4733
<div
4834
className={cn(
4935
'flex flex-col h-full',
50-
isFrozen ? 'select-none pointer-events-none blur' : ''
36+
card.isFrozen ? 'select-none pointer-events-none blur' : ''
5137
)}
5238
>
5339
<div className="flex justify-between text-sm items-center">
@@ -58,18 +44,20 @@ const UserCardFront = ({ name, isFrozen }: UserCardFrontProps) => {
5844
<Chip />
5945
</div>
6046
<div className="flex mt-auto justify-between items-center">
61-
<span className="uppercase opacity-50">{name}</span>
47+
<span className="uppercase opacity-50">{card.name}</span>
6248
<MasterCardLogo />
6349
</div>
6450
</div>
65-
{isFrozen ? (
51+
{card.isFrozen ? (
6652
<div className="absolute inset-0 z-10 bg-[url('/frozen.webp')] bg-cover bg-center opacity-50" />
6753
) : null}
6854
</UserCardContainer>
6955
)
7056
}
7157

7258
const UserCardBack = () => {
59+
const { card } = useCardContext()
60+
7361
return (
7462
<UserCardContainer>
7563
<div className="flex flex-col h-full">
@@ -80,29 +68,29 @@ const UserCardBack = () => {
8068
Card Number
8169
</p>
8270
<div className="flex items-center gap-x-3">
83-
<p className="font-mono">4242 4242 4242 4242</p>
71+
<p className="font-mono">{card.number}</p>
8472
<CopyButton
8573
aria-label="copy card number"
8674
className="h-4 w-4 p-0 opacity-50"
8775
copyType="card"
88-
value="4242 4242 4242 4242"
76+
value={card.number}
8977
/>
9078
</div>
9179
</div>
9280
<div className="flex gap-x-6">
9381
<div>
9482
<p className="leading-3 text-xs font-medium opacity-50">Expiry</p>
95-
<p className="font-mono">01/27</p>
83+
<p className="font-mono">{card.expiry}</p>
9684
</div>
9785
<div>
9886
<p className="leading-3 text-xs font-medium opacity-50">CVV</p>
99-
<p className="font-mono">123</p>
87+
<p className="font-mono">{card.cvv}</p>
10088
</div>
10189
<CopyButton
10290
aria-label="copy cvv"
10391
className="mt-2.5 -ml-3 h-4 w-4 p-0 opacity-50"
10492
copyType="card"
105-
value="123"
93+
value={card.cvv.toString()}
10694
/>
10795
<MasterCardLogo className="ml-auto" />
10896
</div>
@@ -112,17 +100,18 @@ const UserCardBack = () => {
112100
)
113101
}
114102

115-
export const UserCard = ({ type, name }: UserCardProps) => {
103+
interface UserCardProps {
104+
card: IUserCard
105+
}
106+
export const UserCard = ({ card }: UserCardProps) => {
107+
const [showDetails, setShowDetails] = useState(false)
108+
116109
return (
117-
<>
118-
{type === 'normal' || type === 'frozen' ? (
119-
<UserCardFront
120-
name={name}
121-
isFrozen={type === 'frozen' ? true : false}
122-
/>
123-
) : type === 'details' ? (
124-
<UserCardBack />
125-
) : null}
126-
</>
110+
<UserCardContext.Provider value={{ card, showDetails, setShowDetails }}>
111+
{card.isFrozen ? <UserCardFront /> : null}
112+
{!card.isFrozen && showDetails ? <UserCardBack /> : null}
113+
{!card.isFrozen && !showDetails ? <UserCardFront /> : null}
114+
<UserCardActions />
115+
</UserCardContext.Provider>
127116
)
128117
}
Lines changed: 127 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,148 @@
11
import { Button } from '@/ui/Button'
22
import { Eye, EyeCross, Snow, Trash } from '../icons/CardButtons'
3-
import { Dispatch, SetStateAction, useState } from 'react'
4-
import type { CardType } from './UserCard'
3+
import { useCardContext } from './UserCardContext'
4+
import { cardServiceMock } from '@/lib/api/card'
5+
import { useRouter } from 'next/router'
6+
import { Cog } from '../icons/Cog'
57

6-
interface CardActionsProps {
7-
fn: Dispatch<SetStateAction<CardType>>
8+
export const FrozenCardActions = () => {
9+
const router = useRouter()
10+
11+
return (
12+
<>
13+
<div className="flex flex-col gap-y-4">
14+
<Button
15+
intent="primary"
16+
aria-label="unfreeze"
17+
onClick={async () => {
18+
// Maybe use toats for showcasing the result of the api calls,
19+
// specifically for card actions?
20+
// We will probably have a lot more dialogs for card settings
21+
// and using dialogs again for showing the response might be a bit
22+
// cumbersome.
23+
const response = await cardServiceMock.unfreeze()
24+
25+
if (!response.success) {
26+
console.error('[TODO] UPDATE ME - error while unfreezing card')
27+
}
28+
29+
if (response.success) {
30+
router.replace(router.asPath)
31+
}
32+
}}
33+
>
34+
<div className="flex gap-2 justify-center items-center">
35+
<Snow className="size-6" />
36+
</div>
37+
</Button>
38+
<p className="text-center -tracking-wide text-sm">Unfreeze</p>
39+
</div>
40+
<div className="col-span-2 flex flex-col gap-y-4">
41+
<Button
42+
intent="danger"
43+
aria-label="terminate card"
44+
onClick={async () => {
45+
// Maybe use toats for showcasing the result of the api calls,
46+
// specifically for card actions?
47+
// We will probably have a lot more dialogs for card settings
48+
// and using dialogs again for showing the response might be a bit
49+
// cumbersome.
50+
const response = await cardServiceMock.terminate()
51+
52+
if (!response.success) {
53+
console.error('[TODO] UPDATE ME - error while terminating card')
54+
}
55+
56+
if (response.success) {
57+
router.replace(router.asPath)
58+
}
59+
}}
60+
>
61+
<div className="flex gap-2 justify-center items-center">
62+
<Trash className="size-6" />
63+
</div>
64+
</Button>
65+
<p className="text-center -tracking-wide text-sm">Terminate</p>
66+
</div>
67+
</>
68+
)
869
}
970

10-
// TODO: Better naming for the function
11-
export const CardActions = ({ fn }: CardActionsProps) => {
12-
const [isDetailed, setIsDetailed] = useState(false)
13-
const [isFrozen, setIsFrozen] = useState(false)
71+
const DefaultCardActions = () => {
72+
const router = useRouter()
73+
const { showDetails, setShowDetails } = useCardContext()
1474

15-
// ToDO revisit button layout shift, when clicking on butttons
1675
return (
17-
<div className="flex gap-x-3 justify-center items-center">
18-
<Button
19-
intent={isFrozen ? 'primary' : 'secondary'}
20-
aria-label="freeze"
21-
onClick={() => {
22-
setIsFrozen(!isFrozen)
23-
isFrozen ? fn('normal') : fn('frozen')
24-
}}
25-
>
26-
<div className="flex gap-2 justify-center items-center">
27-
<Snow />
28-
{isFrozen ? 'Unfreeze' : 'Freeze'}
29-
</div>
30-
</Button>
31-
{!isFrozen ? (
76+
<>
77+
<div className="flex flex-col gap-y-4">
3278
<Button
33-
aria-label="details"
34-
intent={isDetailed ? 'primary' : 'secondary'}
35-
onClick={() => {
36-
setIsDetailed(!isDetailed)
37-
isDetailed ? fn('normal') : fn('details')
79+
intent="secondary"
80+
aria-label="freeze"
81+
onClick={async () => {
82+
// Maybe use toats for showcasing the result of the api calls,
83+
// specifically for card actions?
84+
// We will probably have a lot more dialogs for card settings
85+
// and using dialogs again for showing the response might be a bit
86+
// cumbersome.
87+
const response = await cardServiceMock.freeze()
88+
89+
if (!response.success) {
90+
console.error('[TODO] UPDATE ME - error while freezing card')
91+
}
92+
93+
if (response.success) {
94+
router.replace(router.asPath)
95+
}
3896
}}
3997
>
4098
<div className="flex gap-2 justify-center items-center">
41-
{isDetailed ? (
42-
<>
43-
<EyeCross />
44-
Hide Details
45-
</>
99+
<Snow className="size-6" />
100+
</div>
101+
</Button>
102+
<p className="text-center -tracking-wide text-sm">Freeze</p>
103+
</div>
104+
<div className="flex flex-col gap-y-4">
105+
<Button
106+
intent="secondary"
107+
aria-label={showDetails ? 'hide details' : 'show details'}
108+
onClick={() => setShowDetails((prev) => !prev)}
109+
>
110+
<div className="flex gap-2 justify-center items-center">
111+
{showDetails ? (
112+
<EyeCross className="size-6" />
46113
) : (
47-
<>
48-
<Eye />
49-
Details
50-
</>
114+
<Eye className="size-6" />
51115
)}
52116
</div>
53117
</Button>
54-
) : (
55-
<Button intent="danger" aria-label="terminate">
118+
<p className="text-center -tracking-wide text-sm">
119+
{showDetails ? 'Hide Details' : 'Details'}
120+
</p>
121+
</div>
122+
<div className="flex flex-col gap-y-4">
123+
<Button
124+
intent="secondary"
125+
aria-label="settings"
126+
onClick={() => {
127+
// TODO: TBD
128+
}}
129+
>
56130
<div className="flex gap-2 justify-center items-center">
57-
<Trash />
58-
Terminate
131+
<Cog className="size-6" />
59132
</div>
60133
</Button>
61-
)}
134+
<p className="text-center -tracking-wide text-sm">Settings</p>
135+
</div>
136+
</>
137+
)
138+
}
139+
140+
export const UserCardActions = () => {
141+
const { card } = useCardContext()
142+
143+
return (
144+
<div className="grid grid-cols-3 gap-x-3">
145+
{card.isFrozen ? <FrozenCardActions /> : <DefaultCardActions />}
62146
</div>
63147
)
64148
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { IUserCard } from '@/lib/api/card'
2+
import {
3+
createContext,
4+
useContext,
5+
type Dispatch,
6+
type SetStateAction
7+
} from 'react'
8+
9+
interface UserCardContextValue {
10+
showDetails: boolean
11+
setShowDetails: Dispatch<SetStateAction<boolean>>
12+
card: IUserCard
13+
}
14+
15+
export const UserCardContext = createContext({} as UserCardContextValue)
16+
17+
export const useCardContext = () => {
18+
const cardContext = useContext(UserCardContext)
19+
20+
if (!cardContext) {
21+
throw new Error(
22+
'"useCardContext" is used outside the UserCardContext provider.'
23+
)
24+
}
25+
26+
return cardContext
27+
}

0 commit comments

Comments
 (0)