ui: change language selection ui

This commit is contained in:
nick-delirium 2025-03-24 11:08:35 +01:00 committed by Delirium
parent e04c2aa251
commit d77a518cf0
10 changed files with 208 additions and 146 deletions

View file

@ -3,6 +3,7 @@ import withPageTitle from 'HOCs/withPageTitle';
import { PageTitle } from 'UI';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import LanguageSwitcher from "App/components/LanguageSwitcher";
import Settings from './Settings';
import ChangePassword from './ChangePassword';
import styles from './profileSettings.module.css';
@ -20,107 +21,90 @@ function ProfileSettings() {
return (
<div className="bg-white rounded-lg border shadow-sm p-5">
<PageTitle title={<div>{t('Account')}</div>} />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{t('Profile')}</h4>
<div className={styles.info}>
{t(
'Your email address is your identity on OpenReplay and is used to login.',
)}
</div>
</div>
<div>
<Settings />
</div>
</div>
<Section
title={t('Profile')}
description={t('Your email address is your identity on OpenReplay and is used to login.')}
children={<Settings />}
/>
<div className="border-b my-10" />
{account.hasPassword && (
<>
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{t('Change Password')}</h4>
<div className={styles.info}>
{t('Updating your password from time to time enhances your accounts security.')}
</div>
</div>
<div>
<ChangePassword />
</div>
</div>
<Section
title={t('Change Password')}
description={t('Updating your password from time to time enhaces your accounts security')}
children={<ChangePassword />}
/>
<div className="border-b my-10" />
</>
)}
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{t('Organization API Key')}</h4>
<div className={styles.info}>
{t('Your API key gives you access to an extra set of services.')}
</div>
</div>
<div>
<Api />
</div>
</div>
<Section
title={t('Interface Language')}
description={t('Select the language in which OpenReplay will appear.')}
children={<LanguageSwitcher />}
/>
<Section
title={t('Organization API Key')}
description={t('Your API key gives you access to an extra set of services.')}
children={<Api />}
/>
{isEnterprise && (account.admin || account.superAdmin) && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{t('Tenant Key')}</h4>
<div className={styles.info}>
{t('For SSO (SAML) authentication.')}
</div>
</div>
<div>
<TenantKey />
</div>
</div>
<Section
title={t('Tenant Key')}
description={t('For SSO (SAML) authentication.')}
children={<TenantKey />}
/>
</>
)}
{!isEnterprise && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{t('Data Collection')}</h4>
<div className={styles.info}>
{t('Enables you to control how OpenReplay captures data on your organizations usage to improve our product.')}
</div>
</div>
<div>
<OptOut />
</div>
</div>
<Section
title={t('Data Collection')}
description={t('Enables you to control how OpenReplay captures data on your organizations usage to improve our product.')}
children={<OptOut />}
/>
</>
)}
{account.license && (
<>
<div className="border-b my-10" />
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{t('License')}</h4>
<div className={styles.info}>
{t('License key and expiration date.')}
</div>
</div>
<div>
<Licenses />
</div>
</div>
<Section title={t('License')} description={t('License key and expiration date.')} children={<Licenses />} />
</>
)}
</div>
);
}
function Section({ title, description, children }: {
title: string;
description: string;
children: React.ReactNode;
}) {
return (
<div className="flex items-center">
<div className={styles.left}>
<h4 className="text-lg mb-4">{title}</h4>
<div className={styles.info}>
{description}
</div>
</div>
<div>
{children}
</div>
</div>
)
}
export default withPageTitle('Account - OpenReplay Preferences')(
observer(ProfileSettings),
);

View file

@ -1,9 +1,7 @@
import { Button, Dropdown, MenuProps, Space, Typography } from 'antd';
import React, { useCallback, useState } from 'react';
import { Button, Dropdown, MenuProps, Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CaretDownOutlined } from '@ant-design/icons';
import { Languages } from 'lucide-react';
import { Icon } from '../ui';
import { ChevronDown } from 'lucide-react';
const langs = [
{ code: 'en', label: 'English' },
@ -12,14 +10,25 @@ const langs = [
{ code: 'ru', label: 'Русский' },
{ code: 'zh', label: '中國人' },
];
const langLabels = {
en: 'English',
fr: 'Français',
es: 'Español',
ru: 'Русский',
zh: '中國人',
}
function LanguageSwitcher() {
const { i18n } = useTranslation();
const [selected, setSelected] = React.useState(i18n.language);
const handleChangeLanguage = useCallback((lang: string) => {
i18n.changeLanguage(lang);
localStorage.setItem('i18nextLng', lang);
}, []);
const onChange = (val: string) => {
setSelected(val)
}
const handleChangeLanguage = () => {
void i18n.changeLanguage(selected)
localStorage.setItem('i18nextLng', selected)
}
const menuItems: MenuProps['items'] = langs.map((lang) => ({
key: lang.code,
@ -31,21 +40,31 @@ function LanguageSwitcher() {
}));
return (
<Dropdown
menu={{
items: menuItems,
selectable: true,
defaultSelectedKeys: [i18n.language],
style: {
maxHeight: 500,
overflowY: 'auto',
},
onClick: (e) => handleChangeLanguage(e.key),
}}
placement="bottomLeft"
>
<Button icon={<Languages size={12} />} />
</Dropdown>
<div className={'flex flex-col gap-2 align-start'}>
<div className={'font-semibold'}>{i18n.t('Language')}</div>
<Dropdown
menu={{
items: menuItems,
selectable: true,
defaultSelectedKeys: [i18n.language],
style: {
maxHeight: 500,
overflowY: 'auto',
},
onClick: (e) => onChange(e.key),
}}
>
<Button>
<div className={'flex justify-between items-center gap-8'}>
<span>{langLabels[selected]}</span>
<ChevronDown size={14} />
</div>
</Button>
</Dropdown>
<Button className={'w-fit'} onClick={handleChangeLanguage}>
{i18n.t('Update')}
</Button>
</div>
);
}

View file

@ -0,0 +1,29 @@
import React from 'react'
import {
Languages, X
} from 'lucide-react'
import { Button } from 'antd';
import { useHistory } from "react-router-dom";
import { client } from 'App/routes'
function LangBanner({ onClose }: { onClose: () => void }) {
const history = useHistory()
const onClick = () => {
history.push(client('account'))
}
return (
<div className={'px-4 py-2 bg-yellow flex items-center w-screen gap-2'}>
<div>
OpenReplay now supports French, Russian, Chinese, and Spanish 🎉. Update your language in settings.
</div>
<div className={'ml-auto'} />
<Button icon={<Languages size={14} />} size={'small'} onClick={onClick}>
Change Language
</Button>
<Button icon={<X size={16} />} type={'text'} shape={'circle'} onClick={onClose} size={'small'} />
</div>
)
}
export default LangBanner

View file

@ -1,6 +1,7 @@
import { Layout, Space, Tooltip } from 'antd';
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import LangBanner from './LangBanner';
import { INDEXES } from 'App/constants/zindex';
import Logo from 'App/layout/Logo';
@ -11,14 +12,27 @@ import { useTranslation } from 'react-i18next';
const { Header } = Layout;
const langBannerClosedKey = '__or__langBannerClosed';
const getLangBannerClosed = () => localStorage.getItem(langBannerClosedKey) === '1'
function TopHeader() {
const { userStore, notificationStore, projectsStore, settingsStore } =
useStore();
const { account } = userStore;
const { siteId } = projectsStore;
const { initialDataFetched } = userStore;
const [langBannerClosed, setLangBannerClosed] = React.useState(getLangBannerClosed);
const { t } = useTranslation();
React.useEffect(() => {
const langBannerVal = localStorage.getItem(langBannerClosedKey);
if (langBannerVal === null) {
localStorage.setItem(langBannerClosedKey, '0')
}
if (langBannerVal === '0') {
localStorage.setItem(langBannerClosedKey, '1')
}
}, [])
useEffect(() => {
if (!account.id || initialDataFetched) return;
Promise.all([
@ -29,51 +43,58 @@ function TopHeader() {
});
}, [account]);
const closeLangBanner = () => {
setLangBannerClosed(true);
localStorage.setItem(langBannerClosedKey, '1');
}
return (
<Header
style={{
position: 'sticky',
top: 0,
zIndex: INDEXES.HEADER,
padding: '0 20px',
display: 'flex',
alignItems: 'center',
height: '60px',
}}
className="justify-between"
>
<Space>
<div
onClick={() => {
settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed);
}}
style={{ paddingTop: '4px' }}
className="cursor-pointer xl:block hidden"
>
<Tooltip
title={
settingsStore.menuCollapsed ? t('Show Menu') : t('Hide Menu')
}
mouseEnterDelay={1}
<>
{langBannerClosed ? null : <LangBanner onClose={closeLangBanner} />}
<Header
style={{
position: 'sticky',
top: 0,
zIndex: INDEXES.HEADER,
padding: '0 20px',
display: 'flex',
alignItems: 'center',
height: '60px',
}}
className="justify-between"
>
<Space>
<div
onClick={() => {
settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed);
}}
style={{ paddingTop: '4px' }}
className="cursor-pointer xl:block hidden"
>
<Icon
name={
settingsStore.menuCollapsed
? 'side_menu_closed'
: 'side_menu_open'
<Tooltip
title={
settingsStore.menuCollapsed ? t('Show Menu') : t('Hide Menu')
}
size={20}
/>
</Tooltip>
</div>
mouseEnterDelay={1}
>
<Icon
name={
settingsStore.menuCollapsed
? 'side_menu_closed'
: 'side_menu_open'
}
size={20}
/>
</Tooltip>
</div>
<div className="flex items-center">
<Logo siteId={siteId} />
</div>
</Space>
<div className="flex items-center">
<Logo siteId={siteId} />
</div>
</Space>
<TopRight />
</Header>
<TopRight />
</Header>
</>
);
}

View file

@ -11,18 +11,13 @@ import ProjectDropdown from 'Shared/ProjectDropdown';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
interface Props {
account: any;
spotOnly?: boolean;
}
function TopRight(props: Props) {
function TopRight() {
const { userStore } = useStore();
const spotOnly = userStore.scopeState === 1;
const { account } = userStore;
return (
<Space style={{ lineHeight: '0' }}>
{props.spotOnly ? null : (
{spotOnly ? null : (
<>
<ProjectDropdown />
<GettingStartedProgress />
@ -30,7 +25,6 @@ function TopRight(props: Props) {
<Notifications />
{account.name ? <HealthStatus /> : null}
<LanguageSwitcher />
</>
)}

View file

@ -1498,5 +1498,8 @@
"More attribute": "More attribute",
"More attributes": "More attributes",
"Account settings updated successfully": "Account settings updated successfully",
"Include rage clicks": "Include rage clicks"
"Include rage clicks": "Include rage clicks",
"Interface Language": "Interface Language",
"Select the language in which OpenReplay will appear.": "Select the language in which OpenReplay will appear.",
"Language": "Language"
}

View file

@ -1498,5 +1498,8 @@
"More attribute": "Más atributos",
"More attributes": "Más atributos",
"Account settings updated successfully": "Configuración de la cuenta actualizada correctamente",
"Include rage clicks": "Incluir clics de ira"
"Include rage clicks": "Incluir clics de ira",
"Interface Language": "Idioma de la interfaz",
"Select the language in which OpenReplay will appear.": "Selecciona el idioma en el que aparecerá OpenReplay.",
"Language": "Idioma"
}

View file

@ -1498,5 +1498,8 @@
"More attribute": "Plus d'attributs",
"More attributes": "Plus d'attributs",
"Account settings updated successfully": "Paramètres du compte mis à jour avec succès",
"Include rage clicks": "Inclure les clics de rage"
"Include rage clicks": "Inclure les clics de rage",
"Interface Language": "Langue de l'interface",
"Select the language in which OpenReplay will appear.": "Sélectionnez la langue dans laquelle OpenReplay apparaîtra.",
"Language": "Langue"
}

View file

@ -1498,5 +1498,8 @@
"More attribute": "Еще атрибут",
"More attributes": "Еще атрибуты",
"Account settings updated successfully": "Настройки аккаунта успешно обновлены",
"Include rage clicks": "Включить невыносимые клики"
"Include rage clicks": "Включить невыносимые клики",
"Interface Language": "Язык интерфейса",
"Select the language in which OpenReplay will appear.": "Выберите язык, на котором будет отображаться OpenReplay.",
"Language": "Язык"
}

View file

@ -1498,5 +1498,8 @@
"More attributes": "更多属性",
"More attribute": "更多属性",
"Account settings updated successfully": "帐户设置已成功更新",
"Include rage clicks": "包括点击狂怒"
"Include rage clicks": "包括点击狂怒",
"Interface Language": "界面语言",
"Select the language in which OpenReplay will appear.": "选择 OpenReplay 将显示的语言。",
"Language": "语言"
}