mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-19 13:18:30 +02:00
refactor: externalize LoginCard
This commit is contained in:
parent
3c3e83448c
commit
7bab28c1be
4 changed files with 211 additions and 91 deletions
127
src/components/login/LoginCard.tsx
Normal file
127
src/components/login/LoginCard.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import type { ComponentProps } from 'react'
|
||||
import type { ErrorMessage } from '@joinmarket-webui/joinmarket-api-ts/jm'
|
||||
import { AlertCircleIcon, RefreshCwIcon, WalletIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import { routes } from '@/constants/routes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { LoginForm } from './LoginForm'
|
||||
|
||||
type LoginFormProps = ComponentProps<typeof LoginForm>
|
||||
type LoginCardProps = Omit<LoginFormProps, 'loading' | 'onSubmit'> &
|
||||
Required<Pick<LoginFormProps, 'onSubmit'>> & {
|
||||
isSubmitting: boolean
|
||||
listWalletsFetching: boolean
|
||||
listWalletsLoading: boolean
|
||||
listWalletsError?: ErrorMessage
|
||||
onReloadClick: () => Promise<void>
|
||||
}
|
||||
|
||||
export const LoginCard = ({
|
||||
wallets,
|
||||
activeWallet,
|
||||
makerRunning,
|
||||
coinjoinInProgress,
|
||||
onSubmit,
|
||||
isSubmitting,
|
||||
listWalletsLoading,
|
||||
listWalletsFetching,
|
||||
listWalletsError,
|
||||
onReloadClick,
|
||||
}: LoginCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="flex flex-col items-center space-y-2">
|
||||
<div className="bg-primary/10 mb-4 flex h-12 w-12 items-center justify-center rounded-full">
|
||||
{listWalletsFetching ? (
|
||||
<Spinner className="size-6" />
|
||||
) : (
|
||||
<WalletIcon className="text-primary" onClick={() => void onReloadClick()} />
|
||||
)}
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold">{/*TODO: i18n */}Welcome to Jam</CardTitle>
|
||||
{listWalletsLoading ? (
|
||||
<>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</>
|
||||
) : wallets && wallets.length > 0 ? (
|
||||
<CardDescription>{/*TODO: i18n */}Select a wallet and enter your password to continue.</CardDescription>
|
||||
) : undefined}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{listWalletsError ? (
|
||||
<>
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle>{t('wallets.error_loading_failed')}</AlertTitle>
|
||||
<AlertDescription>{listWalletsError.message || t('global.errors.reason_unknown')}</AlertDescription>
|
||||
</Alert>
|
||||
<Button variant="ghost" size="sm" onClick={() => void onReloadClick()} disabled={listWalletsFetching}>
|
||||
<RefreshCwIcon className={cn({ 'motion-safe:animate-spin': listWalletsFetching })} />
|
||||
{t('global.retry')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{wallets === undefined || listWalletsLoading ? (
|
||||
<>
|
||||
<LoginForm loading />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div> </div>
|
||||
<div> </div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{wallets && wallets.length === 0 ? (
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground text-sm">{t('wallets.subtitle_no_wallets')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<LoginForm
|
||||
wallets={wallets}
|
||||
activeWallet={activeWallet}
|
||||
makerRunning={makerRunning ?? false}
|
||||
coinjoinInProgress={coinjoinInProgress ?? false}
|
||||
disabled={isSubmitting || listWalletsFetching}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
variant={wallets.length === 0 ? 'default' : 'link'}
|
||||
size={wallets.length === 0 ? 'xxl' : 'default'}
|
||||
onClick={() => void navigate(routes.createWallet)}
|
||||
>
|
||||
{t('wallets.button_new_wallet')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={wallets.length === 0 ? 'secondary' : 'link'}
|
||||
size={wallets.length === 0 ? 'xxl' : 'default'}
|
||||
onClick={() => void navigate('/import-wallet')}
|
||||
disabled
|
||||
>
|
||||
{/* TODO: implement "import wallet" */}
|
||||
{t('wallets.button_import_wallet')}
|
||||
<Badge variant="destructive">Not yet implemented.</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
@ -184,8 +184,6 @@ type LoginFormLoadingProps = {
|
|||
}
|
||||
|
||||
type LoginFormProps =
|
||||
| LoginFormLoadingProps
|
||||
| LoginFormComponentProps
|
||||
| (LoginFormLoadingProps & Partial<LoginFormComponentProps>)
|
||||
| ({ loading?: false } & LoginFormComponentProps)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,18 @@
|
|||
import { listwalletsOptions, unlockwalletMutation } from '@joinmarket-webui/joinmarket-api-ts/@tanstack/react-query'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { AlertCircleIcon, RefreshCwIcon, WalletIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import { useStore } from 'zustand'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import { routes } from '@/constants/routes'
|
||||
import { useApiClient } from '@/hooks/useApiClient'
|
||||
import { hashPassword } from '@/lib/hash'
|
||||
import { withQueryDelay } from '@/lib/queryClient'
|
||||
import { cn, sortWallets } from '@/lib/utils'
|
||||
import { sortWallets } from '@/lib/utils'
|
||||
import type { WalletFileName } from '@/lib/utils'
|
||||
import { authStore, type AuthState } from '@/store/authStore'
|
||||
import { jmSessionStore } from '@/store/jmSessionStore'
|
||||
import { LoginForm } from './LoginForm'
|
||||
import { LoginCard } from './LoginCard'
|
||||
|
||||
interface LoginFormData {
|
||||
walletFileName: WalletFileName
|
||||
|
|
@ -107,86 +100,19 @@ const LoginPage = () => {
|
|||
|
||||
return (
|
||||
<div className="from-background to-muted flex min-h-screen items-center justify-center bg-gradient-to-br p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="flex flex-col items-center space-y-2">
|
||||
<div className="bg-primary/10 mb-4 flex h-12 w-12 items-center justify-center rounded-full">
|
||||
{listWalletsFetching ? (
|
||||
<Spinner className="size-6" />
|
||||
) : (
|
||||
<WalletIcon className="text-primary" onClick={() => void listWalletsRefetch()} />
|
||||
)}
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold">{/*TODO: i18n */}Welcome to Jam</CardTitle>
|
||||
{listWalletsLoading ? (
|
||||
<>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</>
|
||||
) : wallets.length > 0 ? (
|
||||
<CardDescription>{/*TODO: i18n */}Select a wallet and enter your password to continue.</CardDescription>
|
||||
) : undefined}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{listWalletsError ? (
|
||||
<>
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle>{t('wallets.error_loading_failed')}</AlertTitle>
|
||||
<AlertDescription>{listWalletsError.message || t('global.errors.reason_unknown')}</AlertDescription>
|
||||
</Alert>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => void listWalletsRefetch()}
|
||||
disabled={listWalletsFetching}
|
||||
>
|
||||
<RefreshCwIcon className={cn({ 'motion-safe:animate-spin': listWalletsFetching })} />
|
||||
{t('global.retry')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{listWalletsLoading ? (
|
||||
<>
|
||||
<LoginForm loading />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div> </div>
|
||||
<div> </div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{wallets.length === 0 ? (
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground text-sm">{t('wallets.subtitle_no_wallets')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<LoginForm
|
||||
wallets={wallets}
|
||||
activeWallet={activeWalletOrNull ?? undefined}
|
||||
makerRunning={makerRunning}
|
||||
coinjoinInProgress={coinjoinInProgress}
|
||||
disabled={login.isPending || listWalletsFetching}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="link" size="sm" onClick={() => void navigate(routes.createWallet)}>
|
||||
{t('wallets.button_new_wallet')}
|
||||
</Button>
|
||||
<Button variant="link" size="sm" onClick={() => void navigate('/import-wallet')} disabled>
|
||||
{/* TODO: implement "import wallet" */}
|
||||
{t('wallets.button_import_wallet')}
|
||||
<Badge variant="destructive">Not yet implemented.</Badge>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<LoginCard
|
||||
wallets={wallets}
|
||||
activeWallet={activeWalletOrNull ?? undefined}
|
||||
makerRunning={makerRunning}
|
||||
coinjoinInProgress={coinjoinInProgress}
|
||||
disabled={login.isPending || listWalletsFetching}
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={login.isPending}
|
||||
listWalletsFetching={listWalletsFetching}
|
||||
listWalletsLoading={listWalletsLoading}
|
||||
listWalletsError={listWalletsError ?? undefined}
|
||||
onReloadClick={async () => void (await listWalletsRefetch())}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
69
src/stories/jam/pages/login/LoginCard.stories.tsx
Normal file
69
src/stories/jam/pages/login/LoginCard.stories.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { LoginCard } from '@/components/login/LoginCard'
|
||||
import type { WalletFileName } from '@/lib/utils'
|
||||
|
||||
const meta: Meta<typeof LoginCard> = {
|
||||
title: 'Page/Login/LoginCard',
|
||||
component: LoginCard,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof LoginCard>
|
||||
|
||||
const WALLETS = [
|
||||
'Satoshi.jmdat',
|
||||
'Wallet With Whitespaces.jmdat',
|
||||
'a.jmdat',
|
||||
'A very long wallet name that should be shortened but show start and end of name.jmdat',
|
||||
] as WalletFileName[]
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
wallets: WALLETS,
|
||||
activeWallet: undefined,
|
||||
makerRunning: false,
|
||||
coinjoinInProgress: false,
|
||||
disabled: false,
|
||||
onSubmit: async () => alert('Submit clicked!'),
|
||||
},
|
||||
argTypes: {
|
||||
activeWallet: {
|
||||
control: 'select',
|
||||
options: [...WALLETS, 'None'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
wallets: [],
|
||||
activeWallet: undefined,
|
||||
makerRunning: false,
|
||||
coinjoinInProgress: false,
|
||||
disabled: false,
|
||||
onSubmit: async () => alert('Submit clicked!'),
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
listWalletsLoading: true,
|
||||
},
|
||||
}
|
||||
export const Fetching: Story = {
|
||||
args: {
|
||||
listWalletsFetching: true,
|
||||
wallets: WALLETS,
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
listWalletsError: {
|
||||
message: 'Wallet loading failed.',
|
||||
error_description: '500 Server Error',
|
||||
},
|
||||
onReloadClick: async () => alert('Reload clicked!'),
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue