mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-15 12:50:48 +02:00
feat: add language selector to login page
This commit is contained in:
parent
813989e7b7
commit
45dbccb036
6 changed files with 109 additions and 18 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import { useState, type ComponentProps } from 'react'
|
||||
import { RefreshCwIcon, WalletIcon } from 'lucide-react'
|
||||
import { LanguagesIcon, RefreshCwIcon, WalletIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { LanguageSelectorDropdownMenu } from '@/components/ui/jam/LanguageSelector'
|
||||
import { WalletLoadErrorAlert } from '@/components/ui/jam/WalletLoadErrorAlert'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
|
|
@ -71,22 +72,36 @@ export const LoginCard = ({
|
|||
<>
|
||||
<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" />
|
||||
) : (
|
||||
<div className="mb-4 grid w-full grid-cols-3">
|
||||
<div></div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="icon-xxl"
|
||||
className="bg-primary/10 text-primary/80 hover:text-primary self-center justify-self-center rounded-full"
|
||||
title={t('global.refresh')}
|
||||
aria-label={t('global.refresh')}
|
||||
onClick={() => void onReloadClick()}
|
||||
disabled={listWalletsFetching}
|
||||
>
|
||||
{listWalletsFetching ? <Spinner className="size-6" /> : <WalletIcon className="size-5" />}
|
||||
<span className="sr-only">{t('global.refresh')}</span>
|
||||
</Button>
|
||||
|
||||
<LanguageSelectorDropdownMenu align="end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-primary h-10 w-10 rounded-full"
|
||||
title={t('global.retry')}
|
||||
aria-label={t('global.retry')}
|
||||
onClick={() => void onReloadClick()}
|
||||
aria-label={t('settings.label_select_language_aria_label')}
|
||||
title={t('settings.label_select_language_title')}
|
||||
className="self-start justify-self-end"
|
||||
>
|
||||
<WalletIcon />
|
||||
<LanguagesIcon className="size-5" />
|
||||
<span className="sr-only">{t('settings.label_select_language')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</LanguageSelectorDropdownMenu>
|
||||
</div>
|
||||
<CardTitle className="text-center text-2xl font-bold break-words">{t('login.title')}</CardTitle>
|
||||
{listWalletsLoading ? (
|
||||
|
|
@ -103,7 +118,7 @@ export const LoginCard = ({
|
|||
) : null}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
{listWalletsError ? (
|
||||
<>
|
||||
<WalletLoadErrorAlert reason={getErrorReason(listWalletsError, t('global.errors.reason_unknown'))} />
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ vi.mock('next-themes', () => ({
|
|||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
i18n: {
|
||||
resolvedLanguage: 'en',
|
||||
},
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
|
@ -123,7 +126,7 @@ vi.mock('@/components/settings/AccountXpubsDialog', () => ({
|
|||
) : null,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/settings/LanguageSelector', () => ({
|
||||
vi.mock('@/components/ui/jam/LanguageSelector', () => ({
|
||||
LanguageSelector: () => <div>language-selector</div>,
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import { useNavigate, type NavigateFunction } from 'react-router-dom'
|
|||
import { useStore } from 'zustand'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { CurrencySymbol } from '@/components/ui/jam/CurrencySymbol'
|
||||
import { LanguageSelector } from '@/components/ui/jam/LanguageSelector'
|
||||
import PageTitle from '@/components/ui/jam/PageTitle'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import { isDebugFeatureEnabled, isDevMode } from '@/constants/debugFeatures'
|
||||
import { JAM_DOCS_URL, JAM_MATRIX_URL, JAM_REPO_URL, JAM_SEED_MODAL_TIMEOUT, JAM_TELEGRAM_URL } from '@/constants/jam'
|
||||
import { routes } from '@/constants/routes'
|
||||
|
|
@ -37,8 +39,6 @@ import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation'
|
|||
import { cn, type WalletFileName } from '@/lib/utils'
|
||||
import { authStore } from '@/store/authStore'
|
||||
import { jamSettingsStore } from '@/store/jamSettingsStore'
|
||||
import { LanguageSelector } from '../ui/jam/LanguageSelector'
|
||||
import { Spinner } from '../ui/spinner'
|
||||
import { AccountXpubsDialog } from './AccountXpubsDialog'
|
||||
import { SeedPhraseDialog } from './SeedPhraseDialog'
|
||||
import { SettingsItem, SettingsLink, SettingsSwitch } from './SettingsItem'
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const buttonVariants = cva(
|
|||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
||||
'icon-lg': 'size-9',
|
||||
'icon-xxl': 'size-12',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ComponentProps } from 'react'
|
||||
import type { ComponentProps, PropsWithChildren } from 'react'
|
||||
import { LanguagesIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
|
|
@ -11,8 +11,22 @@ import {
|
|||
SelectLabel,
|
||||
} from '@/components/ui/select'
|
||||
import languages from '@/i18n/languages'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '../button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../dropdown-menu'
|
||||
|
||||
export const LanguageSelector = (props: ComponentProps<typeof Select> = {}) => {
|
||||
export const LanguageSelector = ({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof Select> & { className?: string } = {}) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const currentLanguage = languages.find((lang) => lang.key === i18n.resolvedLanguage)
|
||||
const currentLanguageDescription = currentLanguage?.description || 'English'
|
||||
|
|
@ -25,7 +39,10 @@ export const LanguageSelector = (props: ComponentProps<typeof Select> = {}) => {
|
|||
}}
|
||||
{...props}
|
||||
>
|
||||
<SelectTrigger className="w-full text-xs sm:min-w-42" aria-label="Select language">
|
||||
<SelectTrigger
|
||||
className={cn('w-full text-xs sm:min-w-42', className)}
|
||||
aria-label={t('settings.label_select_language_aria_label')}
|
||||
>
|
||||
<SelectValue placeholder={currentLanguageDescription} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -44,3 +61,56 @@ export const LanguageSelector = (props: ComponentProps<typeof Select> = {}) => {
|
|||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
export const LanguageSelectorDropdownMenu = ({
|
||||
className,
|
||||
align,
|
||||
loop = true,
|
||||
...props
|
||||
}: PropsWithChildren<
|
||||
ComponentProps<typeof DropdownMenu> & {
|
||||
className?: string
|
||||
align?: ComponentProps<typeof DropdownMenuContent>['align']
|
||||
loop?: ComponentProps<typeof DropdownMenuContent>['loop']
|
||||
}
|
||||
> = {}) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DropdownMenu {...props}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{props.children ?? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={className}
|
||||
aria-label={t('settings.label_select_language_aria_label')}
|
||||
>
|
||||
<LanguagesIcon />
|
||||
{t('settings.label_select_language')}
|
||||
</Button>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full text-xs sm:min-w-56" align={align} loop={loop}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="flex items-center gap-1">
|
||||
<LanguagesIcon className="size-4" />
|
||||
{t('settings.label_select_language')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={i18n.resolvedLanguage}
|
||||
onValueChange={(value) => {
|
||||
void i18n.changeLanguage(value)
|
||||
}}
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<DropdownMenuRadioItem key={language.key} value={language.key}>
|
||||
{language.description}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,6 +293,8 @@
|
|||
"use_address_chunking_enabled": "Address display chunking enabled",
|
||||
"use_address_chunking_disabled": "Address display chunking disabled",
|
||||
"label_select_language": "Language",
|
||||
"label_select_language_title": "Select language",
|
||||
"label_select_language_aria_label": "$t(settings.label_select_language_title)",
|
||||
"show_seed": "Show seed phrase",
|
||||
"show_xpubs": "Show account xpubs",
|
||||
"reveal_seed": "Reveal seed phrase",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue