0
Fork 0
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:
wangsijie 2022-04-12 17:09:40 +08:00
parent 2f22a81a8f
commit 40d4190e6f
No known key found for this signature in database
GPG key ID: C72642FE24F7D42B
7 changed files with 201 additions and 24 deletions

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,7 @@
@use '@/scss/underscore' as _;
.title {
color: var(--color-neutral-variant-60);
font: var(--font-subhead-cap);
margin-top: _.unit(8);
}

View file

@ -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);

View file

@ -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>
); );

View file

@ -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 wont have dark mode if its turned off.', 'Enabling this setting will auto generate the dark mode color. You app wont have dark mode if its 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',

View file

@ -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 wont have dark mode if its turned off.', 'Enabling this setting will auto generate the dark mode color. You app wont have dark mode if its 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',