mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
Merge pull request #324 from logto-io/gao-init-app-form
feat(console): init app create form
This commit is contained in:
commit
00541dda78
23 changed files with 478 additions and 27 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,9 @@
|
|||
"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",
|
||||
"swr": "^1.2.2"
|
||||
},
|
||||
|
@ -39,6 +41,7 @@
|
|||
"@types/lodash.kebabcase": "^4.1.6",
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"eslint": "^8.10.0",
|
||||
"lint-staged": "^11.1.1",
|
||||
"parcel": "^2.3.1",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
.app {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -34,8 +31,11 @@
|
|||
--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%);
|
||||
--color-text-default: #202223;
|
||||
--color-foggy: #888;
|
||||
}
|
||||
|
||||
$font-family: 'SF UI Text', 'SF Pro Display', sans-serif;
|
||||
|
@ -43,9 +43,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};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -11,7 +10,16 @@ type Props = {
|
|||
};
|
||||
|
||||
const AppContent = ({ children, theme }: Props) => {
|
||||
return <div className={classNames(styles.app, styles.web, styles[theme])}>{children}</div>;
|
||||
useEffect(() => {
|
||||
const classes = [styles.web, styles[theme]].filter((value): value is string => Boolean(value));
|
||||
document.body.classList.add(...classes);
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove(...classes);
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
return <div className={styles.app}>{children}</div>;
|
||||
};
|
||||
|
||||
export default AppContent;
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Card = ({ children }: Props) => {
|
||||
return <div className={styles.card}>{children}</div>;
|
||||
const Card = ({ children, className }: Props) => {
|
||||
return <div className={classNames(styles.card, className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Card;
|
||||
|
|
21
packages/console/src/components/FormField/index.module.scss
Normal file
21
packages/console/src/components/FormField/index.module.scss
Normal 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);
|
||||
}
|
||||
}
|
27
packages/console/src/components/FormField/index.tsx
Normal file
27
packages/console/src/components/FormField/index.tsx
Normal 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;
|
40
packages/console/src/components/RadioGroup/Radio.tsx
Normal file
40
packages/console/src/components/RadioGroup/Radio.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { 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>
|
||||
);
|
||||
|
||||
export type Props = {
|
||||
value: string;
|
||||
title: string;
|
||||
name?: string;
|
||||
children?: ReactNode;
|
||||
isChecked?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const Radio = ({ value, title, name, children, isChecked, onClick }: Props) => {
|
||||
return (
|
||||
<div className={classNames(styles.radio, isChecked && styles.checked)} onClick={onClick}>
|
||||
<input 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;
|
68
packages/console/src/components/RadioGroup/index.module.scss
Normal file
68
packages/console/src/components/RadioGroup/index.module.scss
Normal file
|
@ -0,0 +1,68 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.radioGroup {
|
||||
display: flex;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: _.unit(3);
|
||||
}
|
||||
|
||||
svg {
|
||||
opacity: 0%;
|
||||
}
|
||||
|
||||
|
||||
> .radio {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-width: 220px;
|
||||
padding: _.unit(5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: _.unit(4);
|
||||
border: 1px solid var(--color-neutral-90);
|
||||
outline: none;
|
||||
user-select: 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;
|
||||
}
|
||||
}
|
||||
}
|
34
packages/console/src/components/RadioGroup/index.tsx
Normal file
34
packages/console/src/components/RadioGroup/index.tsx
Normal 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';
|
12
packages/console/src/icons/Close.tsx
Normal file
12
packages/console/src/icons/Close.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React, { SVGProps } from 'react';
|
||||
|
||||
const Close = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M7.05727 7.05703C7.57797 6.53633 8.42219 6.53633 8.94289 7.05703L16.0001 14.1142L23.0573 7.05703C23.578 6.53633 24.4222 6.53633 24.9429 7.05703C25.4636 7.57773 25.4636 8.42195 24.9429 8.94265L17.8857 15.9998L24.9429 23.057C25.4636 23.5777 25.4636 24.4219 24.9429 24.9426C24.4222 25.4633 23.578 25.4633 23.0573 24.9426L16.0001 17.8855L8.94289 24.9426C8.42219 25.4633 7.57797 25.4633 7.05727 24.9426C6.53657 24.4219 6.53657 23.5777 7.05727 23.057L14.1145 15.9998L7.05727 8.94265C6.53657 8.42195 6.53657 7.57773 7.05727 7.05703Z"
|
||||
fill="#333333"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Close;
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import App from './App';
|
||||
|
||||
const app = document.querySelector('#app');
|
||||
ReactModal.setAppElement('#app');
|
||||
ReactDOM.render(<App />, app);
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.card {
|
||||
padding: _.unit(8);
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin-left: _.unit(3);
|
||||
}
|
||||
|
||||
> svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: _.unit(8);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { ApplicationType } from '@logto/schemas';
|
||||
import React from 'react';
|
||||
import { useController, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import FormField from '@/components/FormField';
|
||||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
import Close from '@/icons/Close';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
||||
import TypeDescription from '../TypeDescription';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type FormData = {
|
||||
type: ApplicationType;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const CreateForm = ({ onClose }: Props) => {
|
||||
const { handleSubmit, control } = useForm<FormData>();
|
||||
const {
|
||||
field: { onChange, value },
|
||||
} = useController({ name: 'type', control });
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
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}>
|
||||
{Object.values(ApplicationType).map((value) => (
|
||||
<Radio key={value} title={t(`${applicationTypeI18nKey[value]}.title`)} value={value}>
|
||||
<TypeDescription
|
||||
subtitle={t(`${applicationTypeI18nKey[value]}.subtitle`)}
|
||||
description={t(`${applicationTypeI18nKey[value]}.description`)}
|
||||
/>
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormField>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateForm;
|
|
@ -0,0 +1,17 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.subtitle,
|
||||
.description {
|
||||
margin-top: _.unit(3);
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font: var(--font-body);
|
||||
color: var(--color-text-default);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-caption);
|
||||
color: var(--color-foggy);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
subtitle: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const TypeDescription = ({ subtitle, description }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.subtitle}>{subtitle}</div>
|
||||
<div className={styles.description}>{description}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypeDescription;
|
|
@ -1,6 +1,7 @@
|
|||
import { Application } from '@logto/schemas';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
|
@ -9,12 +10,15 @@ import CardTitle from '@/components/CardTitle';
|
|||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
import ImagePlaceholder from '@/components/ImagePlaceholder';
|
||||
import ItemPreview from '@/components/ItemPreview';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { RequestError } from '@/swr';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
||||
import CreateForm from './components/CreateForm';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const Applications = () => {
|
||||
const [isCreateFormOpen, setIsCreateFormOpen] = useState(false);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data, error } = useSWR<Application[], RequestError>('/api/applications');
|
||||
const isLoading = !data && !error;
|
||||
|
@ -23,7 +27,23 @@ const Applications = () => {
|
|||
<Card>
|
||||
<div className={styles.headline}>
|
||||
<CardTitle title="applications.title" subtitle="applications.subtitle" />
|
||||
<Button disabled title="admin_console.applications.create" />
|
||||
<Button
|
||||
title="admin_console.applications.create"
|
||||
onClick={() => {
|
||||
setIsCreateFormOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={isCreateFormOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
>
|
||||
<CreateForm
|
||||
onClose={() => {
|
||||
setIsCreateFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
|
@ -48,7 +68,7 @@ const Applications = () => {
|
|||
<td>
|
||||
<ItemPreview
|
||||
title={name}
|
||||
subtitle={String(t(applicationTypeI18nKey[type]))}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
icon={<ImagePlaceholder />}
|
||||
/>
|
||||
</td>
|
||||
|
|
13
packages/console/src/scss/modal.module.scss
Normal file
13
packages/console/src/scss/modal.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.content {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%, -50%);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
background: rgba(0, 0, 0, 40%);
|
||||
inset: 0;
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
import { AdminConsoleKey } from '@logto/phrases';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
|
||||
export const applicationTypeI18nKey: Record<ApplicationType, AdminConsoleKey> = {
|
||||
export const applicationTypeI18nKey = Object.freeze({
|
||||
[ApplicationType.Native]: 'applications.type.native',
|
||||
[ApplicationType.SPA]: 'applications.type.spa',
|
||||
[ApplicationType.Traditional]: 'applications.type.tranditional',
|
||||
};
|
||||
[ApplicationType.Traditional]: 'applications.type.traditional',
|
||||
} as const);
|
||||
|
|
|
@ -19,6 +19,9 @@ const translation = {
|
|||
copying: 'Copying',
|
||||
copied: 'Copied',
|
||||
},
|
||||
form: {
|
||||
required: 'Required',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: 'Overview',
|
||||
resource_management: 'Resource Management',
|
||||
|
@ -44,11 +47,24 @@ 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',
|
||||
spa: 'Single Page App',
|
||||
tranditional: 'Tranditional Web App',
|
||||
native: {
|
||||
title: 'Native',
|
||||
subtitle: 'Mobile, desktop, CLI and smart device apps running natively.',
|
||||
description: 'E.g.: iOS, Electron, Apple TV apps',
|
||||
},
|
||||
spa: {
|
||||
title: 'Single Page App',
|
||||
subtitle: 'A JavaScript front-end app that uses an API.',
|
||||
description: 'E.g.: Angular, React, Vue',
|
||||
},
|
||||
traditional: {
|
||||
title: 'Tranditional Web',
|
||||
subtitle: 'Traditional web app using redirects.',
|
||||
description: 'E.g.: Node.js, Express, ASP.NET, Java, PHP',
|
||||
},
|
||||
},
|
||||
},
|
||||
api_resources: {
|
||||
|
|
|
@ -21,6 +21,9 @@ const translation = {
|
|||
copying: '拷贝中',
|
||||
copied: '已拷贝',
|
||||
},
|
||||
form: {
|
||||
required: '必填',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: '概览',
|
||||
resource_management: '资源管理',
|
||||
|
@ -46,11 +49,24 @@ 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',
|
||||
spa: 'Single Page App',
|
||||
tranditional: 'Tranditional Web App',
|
||||
native: {
|
||||
title: 'Native',
|
||||
subtitle: 'Mobile, desktop, CLI and smart device apps running natively.',
|
||||
description: 'E.g.: iOS, Electron, Apple TV apps',
|
||||
},
|
||||
spa: {
|
||||
title: 'Single Page App',
|
||||
subtitle: 'A JavaScript front-end app that uses an API.',
|
||||
description: 'E.g.: Angular, React, Vue',
|
||||
},
|
||||
traditional: {
|
||||
title: 'Tranditional Web',
|
||||
subtitle: 'Traditional web app using redirects.',
|
||||
description: 'E.g.: Node.js, Express, ASP.NET, Java, PHP',
|
||||
},
|
||||
},
|
||||
},
|
||||
api_resources: {
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
.content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
background: var(--color-background);
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ importers:
|
|||
'@types/lodash.kebabcase': ^4.1.6
|
||||
'@types/react': ^17.0.14
|
||||
'@types/react-dom': ^17.0.9
|
||||
'@types/react-modal': ^3.13.1
|
||||
classnames: ^2.3.1
|
||||
eslint: ^8.10.0
|
||||
i18next: ^21.6.12
|
||||
|
@ -44,7 +45,9 @@ 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
|
||||
stylelint: ^13.13.1
|
||||
swr: ^1.2.2
|
||||
|
@ -59,7 +62,9 @@ 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
|
||||
swr: 1.2.2_react@17.0.2
|
||||
devDependencies:
|
||||
|
@ -72,6 +77,7 @@ importers:
|
|||
'@types/lodash.kebabcase': 4.1.6
|
||||
'@types/react': 17.0.37
|
||||
'@types/react-dom': 17.0.11
|
||||
'@types/react-modal': 3.13.1
|
||||
eslint: 8.10.0
|
||||
lint-staged: 11.2.6
|
||||
parcel: 2.3.1_postcss@8.4.6
|
||||
|
@ -3566,6 +3572,12 @@ packages:
|
|||
'@types/react': 17.0.37
|
||||
dev: true
|
||||
|
||||
/@types/react-modal/3.13.1:
|
||||
resolution: {integrity: sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==}
|
||||
dependencies:
|
||||
'@types/react': 17.0.37
|
||||
dev: true
|
||||
|
||||
/@types/react-router-dom/5.3.2:
|
||||
resolution: {integrity: sha512-ELEYRUie2czuJzaZ5+ziIp9Hhw+juEw8b7C11YNA4QdLCVbQ3qLi2l4aq8XnlqM7V31LZX8dxUuFUCrzHm6sqQ==}
|
||||
dependencies:
|
||||
|
@ -6004,6 +6016,10 @@ packages:
|
|||
clone-regexp: 2.2.0
|
||||
dev: true
|
||||
|
||||
/exenv/1.2.2:
|
||||
resolution: {integrity: sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=}
|
||||
dev: false
|
||||
|
||||
/exit/0.1.2:
|
||||
resolution: {integrity: sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
@ -11202,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:
|
||||
|
@ -11251,6 +11276,25 @@ packages:
|
|||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
dev: true
|
||||
|
||||
/react-lifecycles-compat/3.0.4:
|
||||
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
||||
dev: false
|
||||
|
||||
/react-modal/3.14.4_react-dom@17.0.2+react@17.0.2:
|
||||
resolution: {integrity: sha512-8surmulejafYCH9wfUmFyj4UfbSJwjcgbS9gf3oOItu4Hwd6ivJyVBETI0yHRhpJKCLZMUtnhzk76wXTsNL6Qg==}
|
||||
engines: {node: '>=8'}
|
||||
peerDependencies:
|
||||
react: ^0.14.0 || ^15.0.0 || ^16 || ^17
|
||||
react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17
|
||||
dependencies:
|
||||
exenv: 1.2.2
|
||||
prop-types: 15.8.1
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
react-lifecycles-compat: 3.0.4
|
||||
warning: 4.0.3
|
||||
dev: false
|
||||
|
||||
/react-refresh/0.9.0:
|
||||
resolution: {integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -13245,6 +13289,12 @@ packages:
|
|||
makeerror: 1.0.12
|
||||
dev: true
|
||||
|
||||
/warning/4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/wcwidth/1.0.1:
|
||||
resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=}
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue