mirror of
https://github.com/logto-io/logto.git
synced 2025-02-24 22:05:56 -05:00
feat(console): contact us (#509)
* feat(console): contact us * refactor: extract Contact from Item * fix: cr * fix: cr
This commit is contained in:
parent
06ea931d64
commit
b8b5840936
10 changed files with 215 additions and 10 deletions
18
packages/console/src/assets/images/slack.svg
Normal file
18
packages/console/src/assets/images/slack.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.3" width="48" height="48" rx="16" fill="#E0E3E3"/>
|
||||
<g clip-path="url(#clip0_2372_51885)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.4243 17.6484H12.843C11.2728 17.6484 10 18.9212 10 20.4914C10 22.0616 11.2728 23.3344 12.843 23.3344H20.4243C21.9944 23.3344 23.2672 22.0616 23.2672 20.4914C23.2672 18.9212 21.9944 17.6484 20.4243 17.6484Z" fill="#36C5F0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.85 20.4946V12.9133C30.85 11.3431 29.5772 10.0703 28.007 10.0703C26.4369 10.0703 25.1641 11.3431 25.1641 12.9133V20.4946C25.1641 22.0647 26.4369 23.3376 28.007 23.3376C29.5772 23.3376 30.85 22.0647 30.85 20.4946Z" fill="#2EB67D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.007 30.9203H35.5883C37.1585 30.9203 38.4313 29.6475 38.4313 28.0774C38.4313 26.5072 37.1585 25.2344 35.5883 25.2344H28.007C26.4369 25.2344 25.1641 26.5072 25.1641 28.0774C25.1641 29.6475 26.4369 30.9203 28.007 30.9203Z" fill="#ECB22E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.582 28.0774V35.6586C17.582 37.2288 18.8548 38.5016 20.425 38.5016C21.9952 38.5016 23.268 37.2288 23.268 35.6586V28.0774C23.268 26.507 21.9952 25.2344 20.425 25.2344C18.8548 25.2344 17.582 26.5072 17.582 28.0774Z" fill="#E01E5A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.425 10.0703C18.8548 10.0703 17.582 11.3431 17.582 12.9133C17.582 14.4835 18.8548 15.7563 20.425 15.7563H23.268V12.9133C23.268 11.3431 21.9952 10.0703 20.425 10.0703Z" fill="#36C5F0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4321 20.4914C38.4321 18.9212 37.1592 17.6484 35.5891 17.6484C34.0189 17.6484 32.7461 18.9212 32.7461 20.4914V23.3344H35.5891C37.1592 23.3344 38.4321 22.0616 38.4321 20.4914Z" fill="#2EB67D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.007 38.4985C29.5772 38.4985 30.85 37.2256 30.85 35.6555C30.85 34.0853 29.5772 32.8125 28.007 32.8125H25.1641V35.6555C25.1641 37.2256 26.4369 38.4985 28.007 38.4985Z" fill="#ECB22E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 28.0774C10 29.6475 11.2728 30.9203 12.843 30.9203C14.4131 30.9203 15.686 29.6475 15.686 28.0774V25.2344H12.843C11.2728 25.2344 10 26.5072 10 28.0774Z" fill="#E01E5A"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2372_51885">
|
||||
<rect width="28.5" height="28.5" fill="white" transform="translate(10 10)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,31 @@
|
|||
import { I18nKey } from '@logto/phrases';
|
||||
|
||||
import slack from '@/assets/images/slack.svg';
|
||||
|
||||
type ContactItem = {
|
||||
icon: string;
|
||||
title: I18nKey;
|
||||
description: I18nKey;
|
||||
label: I18nKey;
|
||||
};
|
||||
|
||||
export const contacts: ContactItem[] = [
|
||||
{
|
||||
title: 'admin_console.contact.slack.title',
|
||||
icon: slack,
|
||||
description: 'admin_console.contact.slack.description',
|
||||
label: 'admin_console.contact.slack.button',
|
||||
},
|
||||
{
|
||||
title: 'admin_console.contact.github.title',
|
||||
icon: slack,
|
||||
description: 'admin_console.contact.github.description',
|
||||
label: 'admin_console.contact.github.button',
|
||||
},
|
||||
{
|
||||
title: 'admin_console.contact.email.title',
|
||||
icon: slack,
|
||||
description: 'admin_console.contact.email.description',
|
||||
label: 'admin_console.contact.email.button',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,33 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.main {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-component-border);
|
||||
border-radius: _.unit(2);
|
||||
padding: _.unit(3) _.unit(4);
|
||||
|
||||
.icon {
|
||||
margin-right: _.unit(6);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
|
||||
.title {
|
||||
font: var(--font-title-small);
|
||||
color: var(--color-component-text);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-component-caption);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import { contacts } from './const';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
const Contact = ({ isOpen, onCancel }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
>
|
||||
<ModalLayout title="contact.title" subtitle="contact.description" onClose={onCancel}>
|
||||
<div className={styles.main}>
|
||||
{contacts.map(({ title, icon, description, label }) => (
|
||||
<div key={title} className={styles.row}>
|
||||
<div className={styles.icon}>
|
||||
<img src={icon} alt={title} />
|
||||
</div>
|
||||
<div className={styles.text}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.description}>{t(description)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="outline" title={label} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ModalLayout>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
|
@ -9,6 +9,10 @@
|
|||
border-radius: _.unit(2);
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
width: calc(100% - _.unit(10));
|
||||
|
||||
.icon {
|
||||
height: _.unit(5);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { ReactChild } from 'react';
|
||||
import React, { ReactChild, ReactNode, useMemo, useState } from 'react';
|
||||
import { TFuncKey, useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
@ -10,18 +10,47 @@ type Props = {
|
|||
icon?: ReactChild;
|
||||
titleKey: TFuncKey<'translation', 'admin_console.tabs'>;
|
||||
isActive?: boolean;
|
||||
modal?: (isOpen: boolean, onCancel: () => void) => ReactNode;
|
||||
};
|
||||
|
||||
const Item = ({ icon, titleKey, isActive = false }: Props) => {
|
||||
const Item = ({ icon, titleKey, modal, isActive = false }: Props) => {
|
||||
const { t } = useTranslation(undefined, {
|
||||
keyPrefix: 'admin_console.tabs',
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{icon && <div className={styles.icon}>{icon}</div>}
|
||||
<div className={styles.title}>{t(titleKey)}</div>
|
||||
</>
|
||||
),
|
||||
[icon, t, titleKey]
|
||||
);
|
||||
|
||||
if (!modal) {
|
||||
return (
|
||||
<Link to={getPath(titleKey)} className={classNames(styles.row, isActive && styles.active)}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={getPath(titleKey)} className={classNames(styles.row, isActive && styles.active)}>
|
||||
{icon && <div className={styles.icon}>{icon}</div>}
|
||||
<div className={styles.title}>{t(titleKey)}</div>
|
||||
</Link>
|
||||
<>
|
||||
<button
|
||||
className={styles.row}
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
{modal(isOpen, () => {
|
||||
setIsOpen(false);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { FC } from 'react';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { TFuncKey } from 'react-i18next';
|
||||
|
||||
import Contact from './components/Contact';
|
||||
import BarGraph from './icons/BarGraph';
|
||||
import Bolt from './icons/Bolt';
|
||||
import Box from './icons/Box';
|
||||
import Cloud from './icons/Cloud';
|
||||
import Connection from './icons/Connection';
|
||||
import Contact from './icons/Contact';
|
||||
import ContactIcon from './icons/Contact';
|
||||
import Document from './icons/Document';
|
||||
import List from './icons/List';
|
||||
import UserProfile from './icons/UserProfile';
|
||||
|
@ -15,6 +16,7 @@ import Web from './icons/Web';
|
|||
type SidebarItem = {
|
||||
Icon: FC;
|
||||
title: TFuncKey<'translation', 'admin_console.tabs'>;
|
||||
modal?: (isOpen: boolean, onCancel: () => void) => ReactNode;
|
||||
};
|
||||
|
||||
type SidebarSection = {
|
||||
|
@ -74,8 +76,9 @@ export const sections: SidebarSection[] = [
|
|||
title: 'help_and_support',
|
||||
items: [
|
||||
{
|
||||
Icon: Contact,
|
||||
Icon: ContactIcon,
|
||||
title: 'contact_us',
|
||||
modal: (isOpen, onCancel) => <Contact isOpen={isOpen} onCancel={onCancel} />,
|
||||
},
|
||||
{
|
||||
Icon: Document,
|
|
@ -19,12 +19,13 @@ const Sidebar = () => {
|
|||
<div className={styles.sidebar}>
|
||||
{sections.map(({ title, items }) => (
|
||||
<Section key={title} title={t(title)}>
|
||||
{items.map(({ title, Icon }) => (
|
||||
{items.map(({ title, Icon, modal }) => (
|
||||
<Item
|
||||
key={title}
|
||||
titleKey={title}
|
||||
icon={<Icon />}
|
||||
isActive={location.pathname.startsWith(getPath(title))}
|
||||
modal={modal}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
|
|
|
@ -293,6 +293,25 @@ const translation = {
|
|||
not_connected: 'The user is not connected to any social connector.',
|
||||
},
|
||||
},
|
||||
contact: {
|
||||
title: 'Contact us',
|
||||
description: 'You can contact us for help and support.',
|
||||
slack: {
|
||||
title: 'Slack channel',
|
||||
description: 'Join our public channel to chat with developers.',
|
||||
button: 'Join',
|
||||
},
|
||||
github: {
|
||||
title: 'GitHub',
|
||||
description: 'Create an issue.',
|
||||
button: 'Contact',
|
||||
},
|
||||
email: {
|
||||
title: 'Send us an email',
|
||||
description: 'If you have any question.',
|
||||
button: 'Send',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -290,6 +290,25 @@ const translation = {
|
|||
not_connected: '该用户还没有绑定社交账号。',
|
||||
},
|
||||
},
|
||||
contact: {
|
||||
title: 'Contact us',
|
||||
description: 'You can contact us for help and support.',
|
||||
slack: {
|
||||
title: 'Slack channel',
|
||||
description: 'Join our public channel to chat with developers.',
|
||||
button: 'Join',
|
||||
},
|
||||
github: {
|
||||
title: 'GitHub',
|
||||
description: 'Create an issue.',
|
||||
button: 'Contact',
|
||||
},
|
||||
email: {
|
||||
title: 'Send us an email',
|
||||
description: 'If you have any question.',
|
||||
button: 'Send',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue