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 (
+
+ );
+ }
+);
+
+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: