0
Fork 0
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:
Wang Sijie 2022-04-11 18:02:37 +08:00 committed by GitHub
parent 06ea931d64
commit b8b5840936
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 215 additions and 10 deletions

View 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

View file

@ -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',
},
];

View file

@ -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);
}
}
}
}

View file

@ -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;

View file

@ -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);

View file

@ -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);
})}
</>
);
};

View file

@ -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,

View file

@ -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>

View file

@ -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',
},
},
},
};

View file

@ -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',
},
},
},
};