diff --git a/src/frontend/src/i18n/LanguageSelector.tsx b/src/frontend/src/i18n/LanguageSelector.tsx new file mode 100644 index 00000000..0254c790 --- /dev/null +++ b/src/frontend/src/i18n/LanguageSelector.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from 'react-i18next' +import { Button, Popover, PopoverList } from '@/primitives' +import { useLanguageLabels } from './useLanguageLabels' + +export const LanguageSelector = () => { + const { t, i18n } = useTranslation() + const { languagesList, currentLanguage } = useLanguageLabels() + return ( + + + {i18n.language} + + { + i18n.changeLanguage(lang) + }} + /> + + ) +} diff --git a/src/frontend/src/i18n/useLanguageLabels.ts b/src/frontend/src/i18n/useLanguageLabels.ts new file mode 100644 index 00000000..c3df1a86 --- /dev/null +++ b/src/frontend/src/i18n/useLanguageLabels.ts @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next' + +const langageLabels: Record = { + en: 'English', + fr: 'Français', + de: 'Deutsch', +} + +export const useLanguageLabels = () => { + const { i18n } = useTranslation() + // cimode is a testing value from i18next, don't include it + const supportedLanguages = (i18n.options.supportedLngs || []).filter( + (lang) => lang !== 'cimode' + ) + const languagesList = supportedLanguages.map((lang) => ({ + value: lang, + label: langageLabels[lang], + })) + return { languagesList, currentLanguage: langageLabels[i18n.language] } +} diff --git a/src/frontend/src/layout/Header.tsx b/src/frontend/src/layout/Header.tsx index 7b319b4c..82f0bde0 100644 --- a/src/frontend/src/layout/Header.tsx +++ b/src/frontend/src/layout/Header.tsx @@ -1,9 +1,11 @@ -import { useTranslation } from 'react-i18next' +import { Link } from 'wouter' import { css } from '@/styled-system/css' import { flex } from '@/styled-system/patterns' +import { Stack } from '@/styled-system/jsx' +import { useTranslation } from 'react-i18next' +import { LanguageSelector } from '@/i18n/LanguageSelector' import { A, Badge, Text } from '@/primitives' import { authUrl, logoutUrl, useUser } from '@/features/auth' -import { Link } from 'wouter' export const Header = () => { const { t } = useTranslation() @@ -21,29 +23,27 @@ export const Header = () => { boxShadow: 'box', })} > - - + + {t('app')} - - - {isLoggedIn === false && {t('login')}} - {!!user && ( - - {user.email} - - {t('logout')} - - - )} - - + + + + + {isLoggedIn === false && {t('login')}} + {!!user && ( + + {user.email} + + {t('logout')} + + + )} + + + ) } diff --git a/src/frontend/src/locales/de/global.json b/src/frontend/src/locales/de/global.json index 733a3f49..b1acc479 100644 --- a/src/frontend/src/locales/de/global.json +++ b/src/frontend/src/locales/de/global.json @@ -9,6 +9,10 @@ "app": "Meet", "login": "Anmelden", "logout": "", + "languageSelector": { + "popoverLabel": "", + "buttonLabel": "" + }, "loading": "", "notFound": { "heading": "" diff --git a/src/frontend/src/locales/en/global.json b/src/frontend/src/locales/en/global.json index 66686d63..331db050 100644 --- a/src/frontend/src/locales/en/global.json +++ b/src/frontend/src/locales/en/global.json @@ -9,6 +9,10 @@ "app": "Meet", "login": "Login", "logout": "Logout", + "languageSelector": { + "popoverLabel": "Choose language", + "buttonLabel": "Change language (currently {{currentLanguage}})" + }, "loading": "Loading…", "notFound": { "heading": "" diff --git a/src/frontend/src/locales/fr/global.json b/src/frontend/src/locales/fr/global.json index 297c2fc5..c74af0ec 100644 --- a/src/frontend/src/locales/fr/global.json +++ b/src/frontend/src/locales/fr/global.json @@ -9,6 +9,10 @@ "app": "Meet", "login": "Se connecter", "logout": "Se déconnecter", + "languageSelector": { + "popoverLabel": "Choix de la langue", + "buttonLabel": "Changer de langue (actuellement {{currentLanguage}})" + }, "loading": "Chargement…", "notFound": { "heading": "Page introuvable" diff --git a/src/frontend/src/primitives/Button.tsx b/src/frontend/src/primitives/Button.tsx index 4e47470c..01b07ea4 100644 --- a/src/frontend/src/primitives/Button.tsx +++ b/src/frontend/src/primitives/Button.tsx @@ -9,44 +9,69 @@ import { cva, type RecipeVariantProps } from '@/styled-system/css' const button = cva({ base: { display: 'inline-block', - paddingX: '1', - paddingY: '0.625', - transition: 'all 200ms', - borderRadius: 8, + transition: 'background 200ms', cursor: 'pointer', + border: '1px solid transparent', + color: 'colorPalette.text', + backgroundColor: 'colorPalette', + '_ra-hover': { + backgroundColor: 'colorPalette.hover', + }, + '_ra-pressed': { + backgroundColor: 'colorPalette.active', + }, }, variants: { + size: { + default: { + borderRadius: 8, + paddingX: '1', + paddingY: '0.625', + }, + sm: { + borderRadius: 4, + paddingX: '0.5', + paddingY: '0.25', + }, + }, variant: { default: { - color: 'control.text', - backgroundColor: 'control', - '_ra-hover': { - backgroundColor: 'control.hover', - }, - '_ra-pressed': { - backgroundColor: 'control.active', - }, + colorPalette: 'control', }, primary: { - color: 'primary.text', - backgroundColor: 'primary', + colorPalette: 'primary', + }, + }, + outline: { + true: { + color: 'colorPalette', + backgroundColor: 'transparent!', + borderColor: 'currentcolor!', '_ra-hover': { - backgroundColor: 'primary.hover', + backgroundColor: 'colorPalette.subtle!', }, '_ra-pressed': { - backgroundColor: 'primary.active', + backgroundColor: 'colorPalette.subtle!', }, }, }, }, + defaultVariants: { + size: 'default', + variant: 'default', + outline: false, + }, }) -type ButtonProps = RecipeVariantProps & - (RACButtonsProps | LinkProps) +export type ButtonProps = RecipeVariantProps & RACButtonsProps -export const Button = (props: ButtonProps) => { +type LinkButtonProps = RecipeVariantProps & LinkProps + +type ButtonOrLinkProps = ButtonProps | LinkButtonProps + +export const Button = (props: ButtonOrLinkProps) => { const [variantProps, componentProps] = button.splitVariantProps(props) - if ((props as LinkProps).href !== undefined) { + if ((props as LinkButtonProps).href !== undefined) { return } return ( diff --git a/src/frontend/src/primitives/PopoverList.tsx b/src/frontend/src/primitives/PopoverList.tsx new file mode 100644 index 00000000..d9aaa4c2 --- /dev/null +++ b/src/frontend/src/primitives/PopoverList.tsx @@ -0,0 +1,61 @@ +import { ReactNode, useContext } from 'react' +import { + ButtonProps, + Button, + OverlayTriggerStateContext, +} from 'react-aria-components' +import { styled } from '@/styled-system/jsx' + +const ListItem = styled(Button, { + base: { + paddingY: 0.125, + paddingX: 0.5, + textAlign: 'left', + width: 'full', + borderRadius: 4, + cursor: 'pointer', + color: 'primary', + '_ra-hover': { + color: 'primary.subtle-text', + backgroundColor: 'primary.subtle', + }, + }, +}) + +/** + * render a Button primitive that shows a popover showing a list of pressable items + */ +export const PopoverList = ({ + onAction, + closeOnAction = true, + items = [], +}: { + closeOnAction?: boolean + onAction: (key: T) => void + items: Array +} & ButtonProps) => { + const popoverState = useContext(OverlayTriggerStateContext)! + return ( + + {items.map((item) => { + const value = typeof item === 'string' ? item : item.value + const label = typeof item === 'string' ? item : item.label + return ( + + { + onAction(value as T) + if (closeOnAction) { + popoverState.close() + } + }} + > + {label} + + + ) + })} + + ) +}
- {user.email} - - {t('logout')} - -
+ {user.email} + + {t('logout')} + +