Skip to content

Commit

Permalink
feat(wallet/frontend): separate card state from side view (#1555)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
raducristianpopa authored Sep 6, 2024
1 parent f914cce commit 1620a87
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 96 deletions.
67 changes: 28 additions & 39 deletions packages/wallet/frontend/src/components/userCards/UserCard.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import type { ComponentProps } from 'react'
import { useState, type ComponentProps } from 'react'
import { CopyButton } from '@/ui/CopyButton'
import { Chip, GateHubLogo, MasterCardLogo } from '../icons/UserCardIcons'
import { cn } from '@/utils/helpers'

const CARD_TYPE = {
normal: 'normal',
details: 'details',
frozen: 'frozen'
} as const

export type CardType = keyof typeof CARD_TYPE

interface UserCardProps {
type: CardType
name: string
}
import type { IUserCard } from '@/lib/api/card'
import { useCardContext, UserCardContext } from './UserCardContext'
import { UserCardActions } from './UserCardActions'

export type UserCardContainerProps = ComponentProps<'div'>

Expand All @@ -36,18 +26,14 @@ const UserCardContainer = ({
)
}

interface UserCardFrontProps {
name: UserCardProps['name']
isFrozen: boolean
}

const UserCardFront = ({ name, isFrozen }: UserCardFrontProps) => {
const UserCardFront = () => {
const { card } = useCardContext()
return (
<UserCardContainer>
<div
className={cn(
'flex flex-col h-full',
isFrozen ? 'select-none pointer-events-none blur' : ''
card.isFrozen ? 'select-none pointer-events-none blur' : ''
)}
>
<div className="flex justify-between text-sm items-center">
Expand All @@ -58,18 +44,20 @@ const UserCardFront = ({ name, isFrozen }: UserCardFrontProps) => {
<Chip />
</div>
<div className="flex mt-auto justify-between items-center">
<span className="uppercase opacity-50">{name}</span>
<span className="uppercase opacity-50">{card.name}</span>
<MasterCardLogo />
</div>
</div>
{isFrozen ? (
{card.isFrozen ? (
<div className="absolute inset-0 z-10 bg-[url('/frozen.webp')] bg-cover bg-center opacity-50" />
) : null}
</UserCardContainer>
)
}

const UserCardBack = () => {
const { card } = useCardContext()

return (
<UserCardContainer>
<div className="flex flex-col h-full">
Expand All @@ -80,29 +68,29 @@ const UserCardBack = () => {
Card Number
</p>
<div className="flex items-center gap-x-3">
<p className="font-mono">4242 4242 4242 4242</p>
<p className="font-mono">{card.number}</p>
<CopyButton
aria-label="copy card number"
className="h-4 w-4 p-0 opacity-50"
copyType="card"
value="4242 4242 4242 4242"
value={card.number}
/>
</div>
</div>
<div className="flex gap-x-6">
<div>
<p className="leading-3 text-xs font-medium opacity-50">Expiry</p>
<p className="font-mono">01/27</p>
<p className="font-mono">{card.expiry}</p>
</div>
<div>
<p className="leading-3 text-xs font-medium opacity-50">CVV</p>
<p className="font-mono">123</p>
<p className="font-mono">{card.cvv}</p>
</div>
<CopyButton
aria-label="copy cvv"
className="mt-2.5 -ml-3 h-4 w-4 p-0 opacity-50"
copyType="card"
value="123"
value={card.cvv.toString()}
/>
<MasterCardLogo className="ml-auto" />
</div>
Expand All @@ -112,17 +100,18 @@ const UserCardBack = () => {
)
}

export const UserCard = ({ type, name }: UserCardProps) => {
interface UserCardProps {
card: IUserCard
}
export const UserCard = ({ card }: UserCardProps) => {
const [showDetails, setShowDetails] = useState(false)

return (
<>
{type === 'normal' || type === 'frozen' ? (
<UserCardFront
name={name}
isFrozen={type === 'frozen' ? true : false}
/>
) : type === 'details' ? (
<UserCardBack />
) : null}
</>
<UserCardContext.Provider value={{ card, showDetails, setShowDetails }}>
{card.isFrozen ? <UserCardFront /> : null}
{!card.isFrozen && showDetails ? <UserCardBack /> : null}
{!card.isFrozen && !showDetails ? <UserCardFront /> : null}
<UserCardActions />
</UserCardContext.Provider>
)
}
170 changes: 127 additions & 43 deletions packages/wallet/frontend/src/components/userCards/UserCardActions.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,148 @@
import { Button } from '@/ui/Button'
import { Eye, EyeCross, Snow, Trash } from '../icons/CardButtons'
import { Dispatch, SetStateAction, useState } from 'react'
import type { CardType } from './UserCard'
import { useCardContext } from './UserCardContext'
import { cardServiceMock } from '@/lib/api/card'
import { useRouter } from 'next/router'
import { Cog } from '../icons/Cog'

interface CardActionsProps {
fn: Dispatch<SetStateAction<CardType>>
export const FrozenCardActions = () => {
const router = useRouter()

return (
<>
<div className="flex flex-col gap-y-4">
<Button
intent="primary"
aria-label="unfreeze"
onClick={async () => {
// Maybe use toats for showcasing the result of the api calls,
// specifically for card actions?
// We will probably have a lot more dialogs for card settings
// and using dialogs again for showing the response might be a bit
// cumbersome.
const response = await cardServiceMock.unfreeze()

if (!response.success) {
console.error('[TODO] UPDATE ME - error while unfreezing card')
}

if (response.success) {
router.replace(router.asPath)
}
}}
>
<div className="flex gap-2 justify-center items-center">
<Snow className="size-6" />
</div>
</Button>
<p className="text-center -tracking-wide text-sm">Unfreeze</p>
</div>
<div className="col-span-2 flex flex-col gap-y-4">
<Button
intent="danger"
aria-label="terminate card"
onClick={async () => {
// Maybe use toats for showcasing the result of the api calls,
// specifically for card actions?
// We will probably have a lot more dialogs for card settings
// and using dialogs again for showing the response might be a bit
// cumbersome.
const response = await cardServiceMock.terminate()

if (!response.success) {
console.error('[TODO] UPDATE ME - error while terminating card')
}

if (response.success) {
router.replace(router.asPath)
}
}}
>
<div className="flex gap-2 justify-center items-center">
<Trash className="size-6" />
</div>
</Button>
<p className="text-center -tracking-wide text-sm">Terminate</p>
</div>
</>
)
}

// TODO: Better naming for the function
export const CardActions = ({ fn }: CardActionsProps) => {
const [isDetailed, setIsDetailed] = useState(false)
const [isFrozen, setIsFrozen] = useState(false)
const DefaultCardActions = () => {
const router = useRouter()
const { showDetails, setShowDetails } = useCardContext()

// ToDO revisit button layout shift, when clicking on butttons
return (
<div className="flex gap-x-3 justify-center items-center">
<Button
intent={isFrozen ? 'primary' : 'secondary'}
aria-label="freeze"
onClick={() => {
setIsFrozen(!isFrozen)
isFrozen ? fn('normal') : fn('frozen')
}}
>
<div className="flex gap-2 justify-center items-center">
<Snow />
{isFrozen ? 'Unfreeze' : 'Freeze'}
</div>
</Button>
{!isFrozen ? (
<>
<div className="flex flex-col gap-y-4">
<Button
aria-label="details"
intent={isDetailed ? 'primary' : 'secondary'}
onClick={() => {
setIsDetailed(!isDetailed)
isDetailed ? fn('normal') : fn('details')
intent="secondary"
aria-label="freeze"
onClick={async () => {
// Maybe use toats for showcasing the result of the api calls,
// specifically for card actions?
// We will probably have a lot more dialogs for card settings
// and using dialogs again for showing the response might be a bit
// cumbersome.
const response = await cardServiceMock.freeze()

if (!response.success) {
console.error('[TODO] UPDATE ME - error while freezing card')
}

if (response.success) {
router.replace(router.asPath)
}
}}
>
<div className="flex gap-2 justify-center items-center">
{isDetailed ? (
<>
<EyeCross />
Hide Details
</>
<Snow className="size-6" />
</div>
</Button>
<p className="text-center -tracking-wide text-sm">Freeze</p>
</div>
<div className="flex flex-col gap-y-4">
<Button
intent="secondary"
aria-label={showDetails ? 'hide details' : 'show details'}
onClick={() => setShowDetails((prev) => !prev)}
>
<div className="flex gap-2 justify-center items-center">
{showDetails ? (
<EyeCross className="size-6" />
) : (
<>
<Eye />
Details
</>
<Eye className="size-6" />
)}
</div>
</Button>
) : (
<Button intent="danger" aria-label="terminate">
<p className="text-center -tracking-wide text-sm">
{showDetails ? 'Hide Details' : 'Details'}
</p>
</div>
<div className="flex flex-col gap-y-4">
<Button
intent="secondary"
aria-label="settings"
onClick={() => {
// TODO: TBD
}}
>
<div className="flex gap-2 justify-center items-center">
<Trash />
Terminate
<Cog className="size-6" />
</div>
</Button>
)}
<p className="text-center -tracking-wide text-sm">Settings</p>
</div>
</>
)
}

export const UserCardActions = () => {
const { card } = useCardContext()

return (
<div className="grid grid-cols-3 gap-x-3">
{card.isFrozen ? <FrozenCardActions /> : <DefaultCardActions />}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IUserCard } from '@/lib/api/card'
import {
createContext,
useContext,
type Dispatch,
type SetStateAction
} from 'react'

interface UserCardContextValue {
showDetails: boolean
setShowDetails: Dispatch<SetStateAction<boolean>>
card: IUserCard
}

export const UserCardContext = createContext({} as UserCardContextValue)

export const useCardContext = () => {
const cardContext = useContext(UserCardContext)

if (!cardContext) {
throw new Error(
'"useCardContext" is used outside the UserCardContext provider.'
)
}

return cardContext
}
Loading

0 comments on commit 1620a87

Please sign in to comment.