0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(console): implement radio input and form

This commit is contained in:
Gao Sun 2022-03-06 15:49:10 +08:00
parent 2196b217fd
commit 295314e693
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
14 changed files with 263 additions and 5 deletions

View file

@ -31,6 +31,7 @@
"pnpm": ">=6" "pnpm": ">=6"
}, },
"alias": { "alias": {
"html-parse-stringify": "html-parse-stringify/dist/html-parse-stringify.module.js" "html-parse-stringify": "html-parse-stringify/dist/html-parse-stringify.module.js",
"react-hook-form": "react-hook-form/dist/index.esm.mjs"
} }
} }

View file

@ -25,6 +25,7 @@
"lodash.kebabcase": "^4.1.1", "lodash.kebabcase": "^4.1.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-hook-form": "^7.27.1",
"react-i18next": "^11.15.4", "react-i18next": "^11.15.4",
"react-modal": "^3.14.4", "react-modal": "^3.14.4",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.2.2",

View file

@ -31,6 +31,7 @@
--color-neutral-90: #e0e3e3; --color-neutral-90: #e0e3e3;
--color-on-secondary-container: #201c00; --color-on-secondary-container: #201c00;
--color-component-caption: #747778; --color-component-caption: #747778;
--color-component-text: #191c1d;
--color-outline: #78767f; --color-outline: #78767f;
--color-table-row-selected: rgba(0, 0, 0, 2%); --color-table-row-selected: rgba(0, 0, 0, 2%);
} }
@ -40,9 +41,11 @@ $font-family: 'SF UI Text', 'SF Pro Display', sans-serif;
.web { .web {
--font-title-medium: 500 16px/24px #{$font-family}; --font-title-medium: 500 16px/24px #{$font-family};
--font-heading-small: 600 24px/32px #{$font-family}; --font-heading-small: 600 24px/32px #{$font-family};
--font-heading: 600 16px/24px #{$font-family};
--font-body: normal 16px/22px #{$font-family}; --font-body: normal 16px/22px #{$font-family};
--font-small-text: normal 12px/16px #{$font-family}; --font-small-text: normal 12px/16px #{$font-family};
--font-caption: normal 14px/20px #{$font-family}; --font-caption: normal 14px/20px #{$font-family};
--font-caption-bold: 600 14px/20px #{$font-family};
--font-button: 500 14px/20px #{$font-family}; --font-button: 500 14px/20px #{$font-family};
--font-subhead-2: 500 14px/20px #{$font-family}; --font-subhead-2: 500 14px/20px #{$font-family};
} }

View file

@ -0,0 +1,21 @@
@use '@/scss/underscore' as _;
.headline {
display: flex;
justify-content: space-between;
margin-bottom: _.unit(1);
&:not(:first-child) {
margin-top: _.unit(6);
}
.title {
font: var(--font-caption-bold);
color: var(--color-component-text);
}
.required {
font: var(--font-body);
color: var(--color-component-caption);
}
}

View file

@ -0,0 +1,27 @@
import { I18nKey } from '@logto/phrases';
import React, { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import * as styles from './index.module.scss';
type Props = {
title: I18nKey;
children: ReactNode;
isRequired?: boolean;
};
const FormField = ({ title, children, isRequired }: Props) => {
const { t } = useTranslation();
return (
<>
<div className={styles.headline}>
<div className={styles.title}>{t(title)}</div>
{isRequired && <div className={styles.required}>{t('admin_console.form.required')}</div>}
</div>
{children}
</>
);
};
export default FormField;

View file

@ -0,0 +1,52 @@
import classNames from 'classnames';
import React, { forwardRef, ReactNode } from 'react';
import * as styles from './index.module.scss';
const Check = () => (
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="18" height="18" rx="9" fill="#4F37F9" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.31476 13.858L5.13295 10.441C4.95568 10.253 4.95568 9.947 5.13295 9.757L5.77568 9.074C5.95295 8.886 6.24113 8.886 6.4184 9.074L8.63657 11.466L13.5811 6.141C13.7584 5.953 14.0465 5.953 14.2238 6.141L14.8665 6.825C15.0438 7.013 15.0438 7.32 14.8665 7.507L8.95748 13.858C8.78021 14.046 8.49203 14.046 8.31476 13.858Z"
fill="white"
/>
</svg>
);
// https://github.com/yannickcr/eslint-plugin-react/issues/2856
/* eslint-disable react/require-default-props */
export type Props = {
value: string;
title: string;
name?: string;
children?: ReactNode;
isChecked?: boolean;
onClick?: () => void;
};
/* eslint-enable react/require-default-props */
const Radio = forwardRef<HTMLInputElement, Props>(
({ value, title, name, children, isChecked, onClick }, reference) => {
return (
<div className={classNames(styles.radio, isChecked && styles.checked)} onClick={onClick}>
<input
ref={reference}
readOnly
type="radio"
name={name}
value={value}
checked={isChecked}
/>
<div className={classNames(styles.headline, !children && styles.center)}>
<div className={styles.title}>{title}</div>
<Check />
</div>
{children}
</div>
);
}
);
export default Radio;

View file

@ -0,0 +1,67 @@
@use '@/scss/underscore' as _;
.radioGroup {
display: flex;
&:not(:first-child) {
margin-top: _.unit(3);
}
svg {
opacity: 0%;
}
> .radio {
position: relative;
flex: 1;
min-width: 220px;
padding: _.unit(5);
display: flex;
flex-direction: column;
border-radius: _.unit(4);
border: 1px solid var(--color-neutral-90);
outline: none;
cursor: pointer;
&:not(:first-child) {
margin-left: _.unit(6);
}
&.checked {
border-color: var(--color-primary);
outline: 1px solid var(--color-primary);
svg {
opacity: 100%;
}
}
.headline {
flex: 1;
display: flex;
justify-content: space-between;
&.center {
align-items: center;
}
> *:not(:first-child) {
margin-left: _.unit(3);
}
}
.title {
font: var(--font-heading);
color: var(--color-component-text);
}
input[type='radio'] {
appearance: none;
position: absolute;
margin: 0;
width: 0;
height: 0;
}
}
}

View file

@ -0,0 +1,34 @@
import React, { Children, cloneElement, isValidElement, ReactNode } from 'react';
import Radio, { Props as RadioProps } from './Radio';
import * as styles from './index.module.scss';
type Props = {
name: string;
children: ReactNode;
value: string;
onChange?: (value: string) => void;
};
const RadioGroup = ({ name, children, value, onChange }: Props) => {
return (
<div className={styles.radioGroup}>
{Children.map(children, (child) => {
if (!isValidElement(child) || child.type !== Radio) {
return child;
}
return cloneElement<RadioProps>(child, {
name,
isChecked: value === child.props.value,
onClick: () => {
onChange?.(child.props.value);
},
});
})}
</div>
);
};
export default RadioGroup;
export { default as Radio } from './Radio';

View file

@ -16,3 +16,7 @@
cursor: pointer; cursor: pointer;
} }
} }
.form {
margin-top: _.unit(8);
}

View file

@ -1,7 +1,11 @@
import { ApplicationType } from '@logto/schemas';
import React, { SVGProps } from 'react'; import React, { SVGProps } from 'react';
import { useController, useForm } from 'react-hook-form';
import Card from '@/components/Card'; import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle'; import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -14,19 +18,44 @@ const Close = (props: SVGProps<SVGSVGElement>) => (
</svg> </svg>
); );
type FormData = {
type: ApplicationType;
name: string;
description?: string;
};
type Props = { type Props = {
onClose?: () => void; onClose?: () => void;
}; };
const Create = ({ onClose }: Props) => { const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, control } = useForm<FormData>();
const {
field: { onChange, value },
} = useController({ name: 'type', control });
const onSubmit = handleSubmit((data) => {
console.log(data);
});
return ( return (
<Card className={styles.card}> <Card className={styles.card}>
<div className={styles.headline}> <div className={styles.headline}>
<CardTitle title="applications.create" subtitle="applications.subtitle" /> <CardTitle title="applications.create" subtitle="applications.subtitle" />
<Close onClick={onClose} /> <Close onClick={onClose} />
</div> </div>
<form className={styles.form} onSubmit={onSubmit}>
<FormField title="admin_console.applications.select_application_type">
<RadioGroup name="application_type" value={value} onChange={onChange}>
<Radio title="Native" value={ApplicationType.Native} />
<Radio title="Single Page Application" value={ApplicationType.SPA} />
<Radio title="Tranditional Web" value={ApplicationType.Traditional} />
</RadioGroup>
</FormField>
<button type="submit">Submit</button>
</form>
</Card> </Card>
); );
}; };
export default Create; export default CreateForm;

View file

@ -14,7 +14,7 @@ import * as modalStyles from '@/scss/modal.module.scss';
import { RequestError } from '@/swr'; import { RequestError } from '@/swr';
import { applicationTypeI18nKey } from '@/types/applications'; import { applicationTypeI18nKey } from '@/types/applications';
import Create from './components/Create'; import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const Applications = () => { const Applications = () => {
@ -38,7 +38,7 @@ const Applications = () => {
className={modalStyles.content} className={modalStyles.content}
overlayClassName={modalStyles.overlay} overlayClassName={modalStyles.overlay}
> >
<Create <CreateForm
onClose={() => { onClose={() => {
setIsCreateFormOpen(false); setIsCreateFormOpen(false);
}} }}

View file

@ -19,6 +19,9 @@ const translation = {
copying: 'Copying', copying: 'Copying',
copied: 'Copied', copied: 'Copied',
}, },
form: {
required: 'Required',
},
tab_sections: { tab_sections: {
overview: 'Overview', overview: 'Overview',
resource_management: 'Resource Management', resource_management: 'Resource Management',
@ -44,6 +47,7 @@ const translation = {
'Setup a mobile, single page or traditional application to use Logto for authentication.', 'Setup a mobile, single page or traditional application to use Logto for authentication.',
create: 'Create Application', create: 'Create Application',
application_name: 'Application Name', application_name: 'Application Name',
select_application_type: 'Select an application type',
client_id: 'Client ID', client_id: 'Client ID',
type: { type: {
native: 'Native App', native: 'Native App',

View file

@ -21,6 +21,9 @@ const translation = {
copying: '拷贝中', copying: '拷贝中',
copied: '已拷贝', copied: '已拷贝',
}, },
form: {
required: '必填',
},
tab_sections: { tab_sections: {
overview: '概览', overview: '概览',
resource_management: '资源管理', resource_management: '资源管理',
@ -46,6 +49,7 @@ const translation = {
'Setup a mobile, single page or traditional application to use Logto for authentication.', 'Setup a mobile, single page or traditional application to use Logto for authentication.',
create: 'Create Application', create: 'Create Application',
application_name: 'Application Name', application_name: 'Application Name',
select_application_type: 'Select an application type',
client_id: 'Client ID', client_id: 'Client ID',
type: { type: {
native: 'Native App', native: 'Native App',

View file

@ -45,6 +45,7 @@ importers:
prettier: ^2.3.2 prettier: ^2.3.2
react: ^17.0.2 react: ^17.0.2
react-dom: ^17.0.2 react-dom: ^17.0.2
react-hook-form: ^7.27.1
react-i18next: ^11.15.4 react-i18next: ^11.15.4
react-modal: ^3.14.4 react-modal: ^3.14.4
react-router-dom: ^6.2.2 react-router-dom: ^6.2.2
@ -61,6 +62,7 @@ importers:
lodash.kebabcase: 4.1.1 lodash.kebabcase: 4.1.1
react: 17.0.2 react: 17.0.2
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
react-hook-form: 7.27.1_react@17.0.2
react-i18next: 11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5 react-i18next: 11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5
react-modal: 3.14.4_react-dom@17.0.2+react@17.0.2 react-modal: 3.14.4_react-dom@17.0.2+react@17.0.2
react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2 react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2
@ -11216,6 +11218,15 @@ packages:
scheduler: 0.20.2 scheduler: 0.20.2
dev: false dev: false
/react-hook-form/7.27.1_react@17.0.2:
resolution: {integrity: sha512-N3a7A6zIQ8DJeThisVZGtOUabTbJw+7DHJidmB9w8m3chckv2ZWKb5MHps9d2pPJqmCDoWe53Bos56bYmJms5w==}
engines: {node: '>=12.22.0'}
peerDependencies:
react: ^16.8.0 || ^17
dependencies:
react: 17.0.2
dev: false
/react-i18next/11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5: /react-i18next/11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5:
resolution: {integrity: sha512-jKJNAcVcbPGK+yrTcXhLblgPY16n6NbpZZL3Mk8nswj1v3ayIiUBVDU09SgqnT+DluyQBS97hwSvPU5yVFG0yg==} resolution: {integrity: sha512-jKJNAcVcbPGK+yrTcXhLblgPY16n6NbpZZL3Mk8nswj1v3ayIiUBVDU09SgqnT+DluyQBS97hwSvPU5yVFG0yg==}
peerDependencies: peerDependencies: