mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(console): add experience form fields
This commit is contained in:
parent
2f22a81a8f
commit
40d4190e6f
7 changed files with 201 additions and 24 deletions
|
@ -0,0 +1,70 @@
|
||||||
|
import { BrandingStyle, SignInExperience } from '@logto/schemas';
|
||||||
|
import React from 'react';
|
||||||
|
import { Control, Controller, UseFormRegister, UseFormWatch } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import FormField from '@/components/FormField';
|
||||||
|
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||||
|
import TextInput from '@/components/TextInput';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
register: UseFormRegister<SignInExperience>;
|
||||||
|
control: Control<SignInExperience>;
|
||||||
|
watch: UseFormWatch<SignInExperience>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BrandingForm = ({ register, control, watch }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
const isDarkModeEnabled = watch('branding.isDarkModeEnabled');
|
||||||
|
const style = watch('branding.style');
|
||||||
|
const isSloganRequired = style === BrandingStyle.Logo_Slogan;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.title}>{t('sign_in_exp.branding.title')}</div>
|
||||||
|
<FormField isRequired title="admin_console.sign_in_exp.branding.primary_color">
|
||||||
|
{/* TODO: LOG-1733 color-picker */}
|
||||||
|
<TextInput {...register('branding.primaryColor', { required: true })} />
|
||||||
|
</FormField>
|
||||||
|
<FormField isRequired title="admin_console.sign_in_exp.branding.dark_mode">
|
||||||
|
{/* TODO: LOG-2152 switch */}
|
||||||
|
<TextInput {...register('branding.isDarkModeEnabled', { required: true })} />
|
||||||
|
<div>{t('sign_in_exp.branding.dark_mode_description')}</div>
|
||||||
|
</FormField>
|
||||||
|
<FormField
|
||||||
|
isRequired={isDarkModeEnabled}
|
||||||
|
title="admin_console.sign_in_exp.branding.dark_primary_color"
|
||||||
|
>
|
||||||
|
{/* TODO: LOG-1733 color-picker */}
|
||||||
|
<TextInput {...register('branding.darkPrimaryColor', { required: isDarkModeEnabled })} />
|
||||||
|
</FormField>
|
||||||
|
<FormField isRequired title="admin_console.sign_in_exp.branding.ui_style">
|
||||||
|
{/* TODO: LOG-2153 plain radio */}
|
||||||
|
<Controller
|
||||||
|
name="branding.style"
|
||||||
|
control={control}
|
||||||
|
defaultValue={BrandingStyle.Logo_Slogan}
|
||||||
|
render={({ field: { onChange, value, name } }) => (
|
||||||
|
<RadioGroup value={value} name={name} onChange={onChange}>
|
||||||
|
<Radio value={BrandingStyle.Logo_Slogan}>
|
||||||
|
{t('sign_in_exp.branding.styles.logo_slogan')}
|
||||||
|
</Radio>
|
||||||
|
<Radio value={BrandingStyle.Logo}>{t('sign_in_exp.branding.styles.logo')}</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField isRequired title="admin_console.sign_in_exp.branding.logo_image_url">
|
||||||
|
<TextInput {...register('branding.logoUrl', { required: true })} />
|
||||||
|
</FormField>
|
||||||
|
<FormField isRequired={isSloganRequired} title="admin_console.sign_in_exp.branding.slogan">
|
||||||
|
<TextInput {...register('branding.slogan', { required: isSloganRequired })} />
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BrandingForm;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { SignInExperience } from '@logto/schemas';
|
||||||
|
import React from 'react';
|
||||||
|
import { UseFormRegister, UseFormWatch } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import FormField from '@/components/FormField';
|
||||||
|
import TextInput from '@/components/TextInput';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
register: UseFormRegister<SignInExperience>;
|
||||||
|
watch: UseFormWatch<SignInExperience>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TermsForm = ({ register, watch }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const enabled = watch('termsOfUse.enabled');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.title}>{t('sign_in_exp.terms_of_use.title')}</div>
|
||||||
|
<FormField isRequired title="admin_console.sign_in_exp.terms_of_use.enable">
|
||||||
|
<TextInput {...register('termsOfUse.enabled', { required: true })} />
|
||||||
|
<div>{t('sign_in_exp.terms_of_use.description')}</div>
|
||||||
|
</FormField>
|
||||||
|
<FormField isRequired={enabled} title="admin_console.sign_in_exp.terms_of_use.terms_of_use">
|
||||||
|
<TextInput {...register('termsOfUse.contentUrl', { required: enabled })} />
|
||||||
|
</FormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TermsForm;
|
|
@ -0,0 +1,7 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--color-neutral-variant-60);
|
||||||
|
font: var(--font-subhead-cap);
|
||||||
|
margin-top: _.unit(8);
|
||||||
|
}
|
|
@ -7,6 +7,8 @@
|
||||||
.setup {
|
.setup {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-right: _.unit(3);
|
margin-right: _.unit(3);
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
padding-top: _.unit(2);
|
padding-top: _.unit(2);
|
||||||
|
|
|
@ -1,36 +1,94 @@
|
||||||
import classNames from 'classnames';
|
import { SignInExperience as SignInExperienceType } from '@logto/schemas';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import CardTitle from '@/components/CardTitle';
|
import CardTitle from '@/components/CardTitle';
|
||||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||||
|
import useApi, { RequestError } from '@/hooks/use-api';
|
||||||
import * as detailsStyles from '@/scss/details.module.scss';
|
import * as detailsStyles from '@/scss/details.module.scss';
|
||||||
|
|
||||||
|
import BrandingForm from './components/BrandingForm';
|
||||||
|
import TermsForm from './components/TermsForm';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
const SignInExperience = () => {
|
const SignInExperience = () => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const { tab } = useParams();
|
||||||
|
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = useForm<SignInExperienceType>();
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset(data);
|
||||||
|
}
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (formData) => {
|
||||||
|
if (!data || isSubmitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedData = await api
|
||||||
|
.patch(`/api/applications/${data.id}`, {
|
||||||
|
json: {
|
||||||
|
...formData,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json<SignInExperienceType>();
|
||||||
|
void mutate(updatedData);
|
||||||
|
toast.success(t('application_details.save_success'));
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<Card className={classNames(detailsStyles.container, styles.setup)}>
|
<div className={styles.setup}>
|
||||||
<CardTitle title="sign_in_exp.title" subtitle="sign_in_exp.description" />
|
<Card className={detailsStyles.container}>
|
||||||
<TabNav className={styles.tabs}>
|
<CardTitle title="sign_in_exp.title" subtitle="sign_in_exp.description" />
|
||||||
<TabNavLink href="/sign-in-experience/experience">
|
<TabNav className={styles.tabs}>
|
||||||
{t('sign_in_exp.tabs.experience')}
|
<TabNavLink href="/sign-in-experience/experience">
|
||||||
</TabNavLink>
|
{t('sign_in_exp.tabs.experience')}
|
||||||
<TabNavLink href="/sign-in-experience/methods">
|
</TabNavLink>
|
||||||
{t('sign_in_exp.tabs.methods')}
|
<TabNavLink href="/sign-in-experience/methods">
|
||||||
</TabNavLink>
|
{t('sign_in_exp.tabs.methods')}
|
||||||
<TabNavLink href="/sign-in-experience/others">{t('sign_in_exp.tabs.others')}</TabNavLink>
|
</TabNavLink>
|
||||||
</TabNav>
|
<TabNavLink href="/sign-in-experience/others">
|
||||||
<div>TODO</div>
|
{t('sign_in_exp.tabs.others')}
|
||||||
<div className={detailsStyles.footer}>
|
</TabNavLink>
|
||||||
<Button type="primary" title="general.save_changes" />
|
</TabNav>
|
||||||
</div>
|
{!data && !error && <div>loading</div>}
|
||||||
</Card>
|
{error && <div>{`error occurred: ${error.body.message}`}</div>}
|
||||||
|
{data && (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
{tab === 'experience' && (
|
||||||
|
<BrandingForm register={register} control={control} watch={watch} />
|
||||||
|
)}
|
||||||
|
{tab === 'experience' && <TermsForm register={register} watch={watch} />}
|
||||||
|
<div className={detailsStyles.footer}>
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
title="general.save_changes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
<Card className={styles.preview}>TODO</Card>
|
<Card className={styles.preview}>TODO</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -328,12 +328,15 @@ const translation = {
|
||||||
branding: {
|
branding: {
|
||||||
title: 'BRANDING',
|
title: 'BRANDING',
|
||||||
primary_color: 'Primary color',
|
primary_color: 'Primary color',
|
||||||
dark_mode: 'Enable dark mode',
|
dark_primary_color: 'Primary color (Dark)',
|
||||||
|
dark_mode: 'Enable Dark Mode',
|
||||||
dark_mode_description:
|
dark_mode_description:
|
||||||
'Enabling this setting will auto generate the dark mode color. You app won’t have dark mode if it’s turned off.',
|
'Enabling this setting will auto generate the dark mode color. You app won’t have dark mode if it’s turned off.',
|
||||||
ui_style: 'Define your UI style',
|
ui_style: 'Define your UI style',
|
||||||
app_logo: 'App Logo',
|
styles: {
|
||||||
text: 'Text',
|
logo_slogan: 'Logo + slogan',
|
||||||
|
logo: 'Logo',
|
||||||
|
},
|
||||||
logo_image_url: 'Logo image URL',
|
logo_image_url: 'Logo image URL',
|
||||||
slogan: 'Slogan',
|
slogan: 'Slogan',
|
||||||
slogan_placeholder: 'Unleash your creativity',
|
slogan_placeholder: 'Unleash your creativity',
|
||||||
|
|
|
@ -324,12 +324,15 @@ const translation = {
|
||||||
branding: {
|
branding: {
|
||||||
title: 'BRANDING',
|
title: 'BRANDING',
|
||||||
primary_color: 'Primary color',
|
primary_color: 'Primary color',
|
||||||
dark_mode: 'Enable dark mode',
|
dark_primary_color: 'Primary color (Dark)',
|
||||||
|
dark_mode: 'Enable Dark Mode',
|
||||||
dark_mode_description:
|
dark_mode_description:
|
||||||
'Enabling this setting will auto generate the dark mode color. You app won’t have dark mode if it’s turned off.',
|
'Enabling this setting will auto generate the dark mode color. You app won’t have dark mode if it’s turned off.',
|
||||||
ui_style: 'Define your UI style',
|
ui_style: 'Define your UI style',
|
||||||
app_logo: 'App Logo',
|
styles: {
|
||||||
text: 'Text',
|
logo_slogan: 'Logo + slogan',
|
||||||
|
logo: 'Logo',
|
||||||
|
},
|
||||||
logo_image_url: 'Logo image URL',
|
logo_image_url: 'Logo image URL',
|
||||||
slogan: 'Slogan',
|
slogan: 'Slogan',
|
||||||
slogan_placeholder: 'Unleash your creativity',
|
slogan_placeholder: 'Unleash your creativity',
|
||||||
|
|
Loading…
Add table
Reference in a new issue