diff --git a/package.json b/package.json index 4474cdbfd..c1a6482fa 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/packages/console/package.json b/packages/console/package.json index 324dd7fa3..b1d050b85 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -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", diff --git a/packages/console/src/components/AppContent/index.module.scss b/packages/console/src/components/AppContent/index.module.scss index 2ec2cb045..79b1638a5 100644 --- a/packages/console/src/components/AppContent/index.module.scss +++ b/packages/console/src/components/AppContent/index.module.scss @@ -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}; } diff --git a/packages/console/src/components/FormField/index.module.scss b/packages/console/src/components/FormField/index.module.scss new file mode 100644 index 000000000..0bcb5a0e9 --- /dev/null +++ b/packages/console/src/components/FormField/index.module.scss @@ -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); + } +} diff --git a/packages/console/src/components/FormField/index.tsx b/packages/console/src/components/FormField/index.tsx new file mode 100644 index 000000000..f3a7ff148 --- /dev/null +++ b/packages/console/src/components/FormField/index.tsx @@ -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 ( + <> +
+
{t(title)}
+ {isRequired &&
{t('admin_console.form.required')}
} +
+ {children} + + ); +}; + +export default FormField; diff --git a/packages/console/src/components/RadioGroup/Radio.tsx b/packages/console/src/components/RadioGroup/Radio.tsx new file mode 100644 index 000000000..0deecfe4c --- /dev/null +++ b/packages/console/src/components/RadioGroup/Radio.tsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import React, { forwardRef, ReactNode } from 'react'; + +import * as styles from './index.module.scss'; + +const Check = () => ( + + + + +); + +// 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( + ({ value, title, name, children, isChecked, onClick }, reference) => { + return ( +
+ +
+
{title}
+ +
+ {children} +
+ ); + } +); + +export default Radio; diff --git a/packages/console/src/components/RadioGroup/index.module.scss b/packages/console/src/components/RadioGroup/index.module.scss new file mode 100644 index 000000000..7b6bad1f6 --- /dev/null +++ b/packages/console/src/components/RadioGroup/index.module.scss @@ -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; + } + } +} diff --git a/packages/console/src/components/RadioGroup/index.tsx b/packages/console/src/components/RadioGroup/index.tsx new file mode 100644 index 000000000..98ee75f2f --- /dev/null +++ b/packages/console/src/components/RadioGroup/index.tsx @@ -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 ( +
+ {Children.map(children, (child) => { + if (!isValidElement(child) || child.type !== Radio) { + return child; + } + + return cloneElement(child, { + name, + isChecked: value === child.props.value, + onClick: () => { + onChange?.(child.props.value); + }, + }); + })} +
+ ); +}; + +export default RadioGroup; +export { default as Radio } from './Radio'; diff --git a/packages/console/src/pages/Applications/components/Create/index.module.scss b/packages/console/src/pages/Applications/components/CreateForm/index.module.scss similarity index 86% rename from packages/console/src/pages/Applications/components/Create/index.module.scss rename to packages/console/src/pages/Applications/components/CreateForm/index.module.scss index df3124c93..171303508 100644 --- a/packages/console/src/pages/Applications/components/Create/index.module.scss +++ b/packages/console/src/pages/Applications/components/CreateForm/index.module.scss @@ -16,3 +16,7 @@ cursor: pointer; } } + +.form { + margin-top: _.unit(8); +} diff --git a/packages/console/src/pages/Applications/components/Create/index.tsx b/packages/console/src/pages/Applications/components/CreateForm/index.tsx similarity index 50% rename from packages/console/src/pages/Applications/components/Create/index.tsx rename to packages/console/src/pages/Applications/components/CreateForm/index.tsx index 88af325c3..2f3d136da 100644 --- a/packages/console/src/pages/Applications/components/Create/index.tsx +++ b/packages/console/src/pages/Applications/components/CreateForm/index.tsx @@ -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) => ( ); +type FormData = { + type: ApplicationType; + name: string; + description?: string; +}; + type Props = { onClose?: () => void; }; -const Create = ({ onClose }: Props) => { +const CreateForm = ({ onClose }: Props) => { + const { handleSubmit, control } = useForm(); + const { + field: { onChange, value }, + } = useController({ name: 'type', control }); + + const onSubmit = handleSubmit((data) => { + console.log(data); + }); + return (
+
+ + + + + + + + +
); }; -export default Create; +export default CreateForm; diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx index f7f7428b5..be4458478 100644 --- a/packages/console/src/pages/Applications/index.tsx +++ b/packages/console/src/pages/Applications/index.tsx @@ -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} > - { setIsCreateFormOpen(false); }} diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 8cad89c16..03bb2aa0d 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -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', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 03e4a2c1f..eeb39bd01 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -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', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa8ab66cd..3224953ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: