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"
},
"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",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.27.1",
"react-i18next": "^11.15.4",
"react-modal": "^3.14.4",
"react-router-dom": "^6.2.2",

View file

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

@ -1,7 +1,11 @@
import { ApplicationType } from '@logto/schemas';
import React, { SVGProps } from 'react';
import { useController, useForm } from 'react-hook-form';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import * as styles from './index.module.scss';
@ -14,19 +18,44 @@ const Close = (props: SVGProps<SVGSVGElement>) => (
</svg>
);
type FormData = {
type: ApplicationType;
name: string;
description?: string;
};
type Props = {
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 (
<Card className={styles.card}>
<div className={styles.headline}>
<CardTitle title="applications.create" subtitle="applications.subtitle" />
<Close onClick={onClose} />
</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>
);
};
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 { applicationTypeI18nKey } from '@/types/applications';
import Create from './components/Create';
import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss';
const Applications = () => {
@ -38,7 +38,7 @@ const Applications = () => {
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
>
<Create
<CreateForm
onClose={() => {
setIsCreateFormOpen(false);
}}

View file

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

View file

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

View file

@ -45,6 +45,7 @@ importers:
prettier: ^2.3.2
react: ^17.0.2
react-dom: ^17.0.2
react-hook-form: ^7.27.1
react-i18next: ^11.15.4
react-modal: ^3.14.4
react-router-dom: ^6.2.2
@ -61,6 +62,7 @@ importers:
lodash.kebabcase: 4.1.1
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-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
@ -11216,6 +11218,15 @@ packages:
scheduler: 0.20.2
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:
resolution: {integrity: sha512-jKJNAcVcbPGK+yrTcXhLblgPY16n6NbpZZL3Mk8nswj1v3ayIiUBVDU09SgqnT+DluyQBS97hwSvPU5yVFG0yg==}
peerDependencies: