ui: change language selection ui
This commit is contained in:
parent
e04c2aa251
commit
d77a518cf0
10 changed files with 208 additions and 146 deletions
|
|
@ -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 account’s security.')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ChangePassword />
|
||||
</div>
|
||||
</div>
|
||||
<Section
|
||||
title={t('Change Password')}
|
||||
description={t('Updating your password from time to time enhaces your account’s 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 organization’s 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 organization’s 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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
29
frontend/app/layout/LangBanner.tsx
Normal file
29
frontend/app/layout/LangBanner.tsx
Normal 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
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "Язык"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "语言"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue