mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(ui): migrate files 1/2 (#251)
This commit is contained in:
parent
9e30b41028
commit
46d1fee353
37 changed files with 1011 additions and 10 deletions
|
@ -28,5 +28,8 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.15.0",
|
"node": ">=14.15.0",
|
||||||
"pnpm": ">=6"
|
"pnpm": ">=6"
|
||||||
|
},
|
||||||
|
"alias": {
|
||||||
|
"html-parse-stringify": "html-parse-stringify/dist/html-parse-stringify.module.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
"stylelint": "stylelint \"src/**/*.scss\""
|
"stylelint": "stylelint \"src/**/*.scss\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logto/phrases": "^0.1.0",
|
||||||
|
"@logto/schemas": "^0.1.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"url": "https://github.com/logto-io/logto/issues"
|
"url": "https://github.com/logto-io/logto/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logto/schemas": "^0.1.0",
|
||||||
"@silverhand/essentials": "^1.1.4"
|
"@silverhand/essentials": "^1.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -8,13 +8,20 @@
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"start": "parcel src/index.html",
|
"start": "parcel src/index.html",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall",
|
"build": "rm -rf dist && parcel build src/index.html --no-autoinstall",
|
||||||
"lint": "eslint --ext .ts --ext .tsx src",
|
|
||||||
"stylelint": "stylelint \"src/**/*.scss\""
|
"stylelint": "stylelint \"src/**/*.scss\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logto/phrases": "^0.1.0",
|
||||||
|
"@logto/schemas": "^0.1.0",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"i18next": "^21.6.11",
|
||||||
|
"i18next-browser-languagedetector": "^6.1.3",
|
||||||
|
"ky": "^0.29.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2",
|
||||||
|
"react-i18next": "^11.15.4",
|
||||||
|
"react-router-dom": "^5.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/core": "^2.3.1",
|
"@parcel/core": "^2.3.1",
|
||||||
|
@ -25,6 +32,7 @@
|
||||||
"@silverhand/ts-config-react": "^0.8.1",
|
"@silverhand/ts-config-react": "^0.8.1",
|
||||||
"@types/react": "^17.0.14",
|
"@types/react": "^17.0.14",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"@types/react-router-dom": "^5.3.2",
|
||||||
"eslint": "^8.1.0",
|
"eslint": "^8.1.0",
|
||||||
"lint-staged": "^11.1.1",
|
"lint-staged": "^11.1.1",
|
||||||
"parcel": "^2.3.1",
|
"parcel": "^2.3.1",
|
||||||
|
@ -33,6 +41,9 @@
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"stylelint": "^13.13.1"
|
"stylelint": "^13.13.1"
|
||||||
},
|
},
|
||||||
|
"alias": {
|
||||||
|
"@/*": "./src/$1"
|
||||||
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "@silverhand/react"
|
"extends": "@silverhand/react"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
.app {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
|
@ -1,7 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Route, Switch, BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import * as styles from './App.module.scss';
|
import AppContent from './components/AppContent';
|
||||||
|
import useTheme from './hooks/use-theme';
|
||||||
|
import initI18n from './i18n/init';
|
||||||
|
import Consent from './pages/Consent';
|
||||||
|
import Register from './pages/Register';
|
||||||
|
import SignIn from './pages/SignIn';
|
||||||
|
import './scss/normalized.scss';
|
||||||
|
|
||||||
export const App = () => {
|
void initI18n();
|
||||||
return <h1 className={styles.app}>Hello world!</h1>;
|
|
||||||
|
const App = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContent theme={theme}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/sign-in" component={SignIn} />
|
||||||
|
<Route exact path="/sign-in/consent" component={Consent} />
|
||||||
|
<Route exact path="/register" component={Register} />
|
||||||
|
</Switch>
|
||||||
|
</BrowserRouter>
|
||||||
|
</AppContent >
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
59
packages/ui-new/src/__mocks__/react-i18next.js
vendored
Normal file
59
packages/ui-new/src/__mocks__/react-i18next.js
vendored
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copied from https://github.com/i18next/react-i18next/blob/c533ffe092470b58769586681abfc2cf4ecf0b56/example/test-jest/src/__mocks__/react-i18next.js
|
||||||
|
/* eslint-disable unicorn/prefer-module */
|
||||||
|
const React = require('react');
|
||||||
|
const reactI18next = require('react-i18next');
|
||||||
|
|
||||||
|
const hasChildren = (node) => node && (node.children || (node.props && node.props.children));
|
||||||
|
|
||||||
|
const getChildren = (node) =>
|
||||||
|
node && node.children ? node.children : node.props && node.props.children;
|
||||||
|
|
||||||
|
const renderNodes = (reactNodes) => {
|
||||||
|
if (typeof reactNodes === 'string') {
|
||||||
|
return reactNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(reactNodes).map((key, i) => {
|
||||||
|
const child = reactNodes[key];
|
||||||
|
const isElement = React.isValidElement(child);
|
||||||
|
|
||||||
|
if (typeof child === 'string') {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildren(child)) {
|
||||||
|
const inner = renderNodes(getChildren(child));
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
return React.cloneElement(child, { ...child.props, key: i }, inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof child === 'object' && !isElement) {
|
||||||
|
return Object.keys(child).reduce((string, childKey) => `${string}${child[childKey]}`, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMock = [(k) => k, {}];
|
||||||
|
useMock.t = (k) => k;
|
||||||
|
useMock.i18n = {};
|
||||||
|
|
||||||
|
const withTranslation = (Component) => (props) => <Component t={(k) => k} {...props} />;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// This mock makes sure any components using the translate HoC receive the t function as a prop
|
||||||
|
withTranslation: () => withTranslation,
|
||||||
|
Trans: ({ children }) =>
|
||||||
|
Array.isArray(children) ? renderNodes(children) : renderNodes([children]),
|
||||||
|
Translation: ({ children }) => children((k) => k, { i18n: {} }),
|
||||||
|
useTranslation: () => useMock,
|
||||||
|
|
||||||
|
// Mock if needed
|
||||||
|
I18nextProvider: reactI18next.I18nextProvider,
|
||||||
|
initReactI18next: reactI18next.initReactI18next,
|
||||||
|
setDefaults: reactI18next.setDefaults,
|
||||||
|
getDefaults: reactI18next.getDefaults,
|
||||||
|
setI18n: reactI18next.setI18n,
|
||||||
|
getI18n: reactI18next.getI18n,
|
||||||
|
};
|
9
packages/ui-new/src/apis/consent.ts
Normal file
9
packages/ui-new/src/apis/consent.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ky from 'ky';
|
||||||
|
|
||||||
|
export const consent = async () => {
|
||||||
|
type Response = {
|
||||||
|
redirectTo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ky.post('/api/session/consent').json<Response>();
|
||||||
|
};
|
16
packages/ui-new/src/apis/register.ts
Normal file
16
packages/ui-new/src/apis/register.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import ky from 'ky';
|
||||||
|
|
||||||
|
export const register = async (username: string, password: string) => {
|
||||||
|
type Response = {
|
||||||
|
redirectTo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ky
|
||||||
|
.post('/api/session/register', {
|
||||||
|
json: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json<Response>();
|
||||||
|
};
|
16
packages/ui-new/src/apis/sign-in.ts
Normal file
16
packages/ui-new/src/apis/sign-in.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import ky from 'ky';
|
||||||
|
|
||||||
|
export const signInBasic = async (username: string, password: string) => {
|
||||||
|
type Response = {
|
||||||
|
redirectTo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ky
|
||||||
|
.post('/api/session', {
|
||||||
|
json: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json<Response>();
|
||||||
|
};
|
71
packages/ui-new/src/components/AppContent/index.module.scss
Normal file
71
packages/ui-new/src/components/AppContent/index.module.scss
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
.content {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.universal {
|
||||||
|
/* Color */
|
||||||
|
--color-gradient: linear-gradient(12.07deg, #3c4ce3 8.81%, #717ce0 93.49%);
|
||||||
|
--color-button-background: #3c4ce3;
|
||||||
|
--color-button-background-disabled: #626fe8;
|
||||||
|
--color-button-background-hover: #2234df;
|
||||||
|
--color-button-text: #f5f5f5;
|
||||||
|
--color-button-text-disabled: #eee;
|
||||||
|
--color-error: #ff6b66;
|
||||||
|
--color-error-background: #{rgba(#ff6b66, 0.15)};
|
||||||
|
--color-error-border: #{rgba(#ff6b66, 0.35)};
|
||||||
|
|
||||||
|
/* Shadow */
|
||||||
|
--shadow-button: 2px 2px 8px rgb(60 76 227 / 25%);
|
||||||
|
|
||||||
|
/* Transition */
|
||||||
|
--transition-default-function: 0.2s ease-in-out;
|
||||||
|
--transition-default-control: background var(--transition-default-function), color var(--transition-default-function);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* Color */
|
||||||
|
--color-heading: #f3f6f9;
|
||||||
|
--color-body: #dadae0;
|
||||||
|
--color-secondary: #b3b7ba;
|
||||||
|
--color-placeholder: #888;
|
||||||
|
--color-control-background: #45484a;
|
||||||
|
--color-control-background-disabled: #65686a;
|
||||||
|
--color-background: #101419;
|
||||||
|
|
||||||
|
/* Shadow */
|
||||||
|
--shadow-card: 2px 2px 24px rgb(59 61 63 / 25%);
|
||||||
|
--shadow-control: 1px 1px 2px rgb(221 221 221 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
/* Color */
|
||||||
|
--color-heading: #333;
|
||||||
|
--color-body: #555;
|
||||||
|
--color-secondary: #888;
|
||||||
|
--color-placeholder: #aaa;
|
||||||
|
--color-control-background: #f5f8fa;
|
||||||
|
--color-control-background-disabled: #eaeaea;
|
||||||
|
--color-background: #fdfdff;
|
||||||
|
|
||||||
|
/* Shadow */
|
||||||
|
--shadow-card: 2px 2px 24px rgb(187 189 191 / 20%);
|
||||||
|
--shadow-control: 1px 1px 2px rgb(221 221 221 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
$font-family: 'PingFang SC', 'SF Pro Text', sans-serif;
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
--font-headline: 600 40px/56px #{$font-family};
|
||||||
|
--font-heading-1: 600 28px/39px #{$font-family};
|
||||||
|
--font-heading-2: 600 20px/28px #{$font-family};
|
||||||
|
--font-heading-3: 600 16px/22.4px #{$font-family};
|
||||||
|
--font-control: 500 16px/22.4px #{$font-family};
|
||||||
|
--font-body: 400 12px/16px #{$font-family};
|
||||||
|
--font-body-bold: 500 12px/16px #{$font-family};
|
||||||
|
}
|
21
packages/ui-new/src/components/AppContent/index.tsx
Normal file
21
packages/ui-new/src/components/AppContent/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
export type Theme = 'dark' | 'light';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
theme: Theme;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppContent = ({ children, theme }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.content, styles.universal, styles.mobile, styles[theme])}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppContent;
|
26
packages/ui-new/src/components/Button/index.module.scss
Normal file
26
packages/ui-new/src/components/Button/index.module.scss
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: _.unit(3) _.unit(8);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--color-button-background);
|
||||||
|
color: var(--color-button-text);
|
||||||
|
font: var(--font-control);
|
||||||
|
box-shadow: var(--shadow-button);
|
||||||
|
transition: var(--transition-default-control);
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--color-button-background-disabled);
|
||||||
|
color: var(--color-button-text-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled):hover {
|
||||||
|
background: var(--color-button-background-hover);
|
||||||
|
}
|
||||||
|
}
|
25
packages/ui-new/src/components/Button/index.tsx
Normal file
25
packages/ui-new/src/components/Button/index.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
value?: string;
|
||||||
|
onClick?: React.MouseEventHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Button = ({ isDisabled, className, value, onClick }: Props) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
disabled={isDisabled}
|
||||||
|
className={classNames(styles.button, className)}
|
||||||
|
type="button"
|
||||||
|
value={value}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
23
packages/ui-new/src/components/Input/index.module.scss
Normal file
23
packages/ui-new/src/components/Input/index.module.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: _.unit(3) _.unit(5);
|
||||||
|
border-radius: _.unit(2);
|
||||||
|
background: var(--color-control-background);
|
||||||
|
color: var(--color-heading);
|
||||||
|
font: var(--font-control);
|
||||||
|
transition: var(--transition-default-control);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--color-control-background-disabled);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
}
|
43
packages/ui-new/src/components/Input/index.tsx
Normal file
43
packages/ui-new/src/components/Input/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
name: string;
|
||||||
|
autoComplete?: AutoCompleteType;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
type?: InputType;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Input = ({
|
||||||
|
name,
|
||||||
|
autoComplete,
|
||||||
|
isDisabled,
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
type = 'text',
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
disabled={isDisabled}
|
||||||
|
className={classNames(styles.input, className)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
onChange={({ target: { value } }) => {
|
||||||
|
onChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
13
packages/ui-new/src/components/MessageBox/index.module.scss
Normal file
13
packages/ui-new/src/components/MessageBox/index.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.messageBox {
|
||||||
|
font: var(--font-body-bold);
|
||||||
|
padding: _.unit(2) _.unit(5);
|
||||||
|
border-radius: _.unit();
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: var(--color-error);
|
||||||
|
background: var(--color-error-background);
|
||||||
|
border: 1px solid var(--color-error-border);
|
||||||
|
}
|
||||||
|
}
|
15
packages/ui-new/src/components/MessageBox/index.tsx
Normal file
15
packages/ui-new/src/components/MessageBox/index.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageBox = ({ className, children }: Props) => {
|
||||||
|
return <div className={classNames(styles.messageBox, styles.error, className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageBox;
|
12
packages/ui-new/src/components/TextLink/index.module.scss
Normal file
12
packages/ui-new/src/components/TextLink/index.module.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--color-button-background);
|
||||||
|
font: var(--font-body-bold);
|
||||||
|
transition: var(--transition-default-control);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-button-background-hover);
|
||||||
|
}
|
||||||
|
}
|
20
packages/ui-new/src/components/TextLink/index.tsx
Normal file
20
packages/ui-new/src/components/TextLink/index.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ReactChild } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
className?: string;
|
||||||
|
children: ReactChild;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextLink = ({ className, children, href }: Props) => {
|
||||||
|
return (
|
||||||
|
<a className={classNames(styles.link, className)} href={href}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextLink;
|
52
packages/ui-new/src/hooks/use-api.ts
Normal file
52
packages/ui-new/src/hooks/use-api.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { RequestErrorBody } from '@logto/schemas';
|
||||||
|
import { HTTPError } from 'ky';
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
type UseApi<T extends any[], U> = {
|
||||||
|
result?: U;
|
||||||
|
loading: boolean;
|
||||||
|
error: RequestErrorBody | null;
|
||||||
|
run: (...args: T) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useApi<Args extends any[], Response>(
|
||||||
|
api: (...args: Args) => Promise<Response>
|
||||||
|
): UseApi<Args, Response> {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<RequestErrorBody | null>(null);
|
||||||
|
const [result, setResult] = useState<Response>();
|
||||||
|
|
||||||
|
const run = useCallback(
|
||||||
|
async (...args: Args) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api(...args);
|
||||||
|
setResult(result);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof HTTPError) {
|
||||||
|
const kyError = await error.response.json<RequestErrorBody>();
|
||||||
|
setError(kyError);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[api]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
result,
|
||||||
|
run,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useApi;
|
24
packages/ui-new/src/hooks/use-theme.ts
Normal file
24
packages/ui-new/src/hooks/use-theme.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Theme } from '@/components/AppContent';
|
||||||
|
|
||||||
|
const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const getThemeBySystemConfiguration = (): Theme => (darkThemeWatchMedia.matches ? 'dark' : 'light');
|
||||||
|
|
||||||
|
export default function useTheme() {
|
||||||
|
const [theme, setTheme] = useState(getThemeBySystemConfiguration());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const changeTheme = () => {
|
||||||
|
setTheme(getThemeBySystemConfiguration());
|
||||||
|
};
|
||||||
|
|
||||||
|
darkThemeWatchMedia.addEventListener('change', changeTheme);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
darkThemeWatchMedia.removeEventListener('change', changeTheme);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return theme;
|
||||||
|
}
|
18
packages/ui-new/src/i18n/init.ts
Normal file
18
packages/ui-new/src/i18n/init.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import resources from '@logto/phrases';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
const initI18n = async () =>
|
||||||
|
i18next
|
||||||
|
.use(initReactI18next)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default initI18n;
|
76
packages/ui-new/src/include.d/dom.d.ts
vendored
Normal file
76
packages/ui-new/src/include.d/dom.d.ts
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types
|
||||||
|
type InputType =
|
||||||
|
| 'button'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'color'
|
||||||
|
| 'date'
|
||||||
|
| 'datetime-local'
|
||||||
|
| 'email'
|
||||||
|
| 'file'
|
||||||
|
| 'hidden'
|
||||||
|
| 'image'
|
||||||
|
| 'month'
|
||||||
|
| 'number'
|
||||||
|
| 'password'
|
||||||
|
| 'radio'
|
||||||
|
| 'range'
|
||||||
|
| 'reset'
|
||||||
|
| 'search'
|
||||||
|
| 'submit'
|
||||||
|
| 'tel'
|
||||||
|
| 'text'
|
||||||
|
| 'time'
|
||||||
|
| 'url'
|
||||||
|
| 'week';
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
|
||||||
|
type AutoCompleteType =
|
||||||
|
| 'name'
|
||||||
|
| 'honorific-prefix'
|
||||||
|
| 'given-name'
|
||||||
|
| 'additional-name'
|
||||||
|
| 'family-name'
|
||||||
|
| 'honorific-suffix'
|
||||||
|
| 'nickname'
|
||||||
|
| 'username'
|
||||||
|
| 'new-password'
|
||||||
|
| 'current-password'
|
||||||
|
| 'one-time-code'
|
||||||
|
| 'organization-title'
|
||||||
|
| 'organization'
|
||||||
|
| 'street-address'
|
||||||
|
| 'address-line1'
|
||||||
|
| 'address-line2'
|
||||||
|
| 'address-line3'
|
||||||
|
| 'address-level4'
|
||||||
|
| 'address-level3'
|
||||||
|
| 'address-level2'
|
||||||
|
| 'address-level1'
|
||||||
|
| 'country'
|
||||||
|
| 'country-name'
|
||||||
|
| 'postal-code'
|
||||||
|
| 'cc-name'
|
||||||
|
| 'cc-given-name'
|
||||||
|
| 'cc-additional-name'
|
||||||
|
| 'cc-family-name'
|
||||||
|
| 'cc-number'
|
||||||
|
| 'cc-exp'
|
||||||
|
| 'cc-exp-month'
|
||||||
|
| 'cc-exp-year'
|
||||||
|
| 'cc-csc'
|
||||||
|
| 'cc-type'
|
||||||
|
| 'transaction-currency'
|
||||||
|
| 'transaction-amount'
|
||||||
|
| 'language'
|
||||||
|
| 'bday'
|
||||||
|
| 'bday-day'
|
||||||
|
| 'bday-month'
|
||||||
|
| 'bday-year'
|
||||||
|
| 'sex'
|
||||||
|
| 'url'
|
||||||
|
| 'photo';
|
||||||
|
|
||||||
|
// TO-DO: remove me
|
||||||
|
interface Body {
|
||||||
|
json<T>(): Promise<T>;
|
||||||
|
}
|
11
packages/ui-new/src/include.d/react-i18next.d.ts
vendored
Normal file
11
packages/ui-new/src/include.d/react-i18next.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// https://react.i18next.com/latest/typescript#create-a-declaration-file
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-unassigned-import
|
||||||
|
import 'react-i18next';
|
||||||
|
import en from '@logto/phrases/lib/locales/en.js';
|
||||||
|
|
||||||
|
declare module 'react-i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
resources: typeof en;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { App } from './App';
|
import App from './App';
|
||||||
|
|
||||||
const app = document.querySelector('#app');
|
const app = document.querySelector('#app');
|
||||||
ReactDOM.render(<App />, app);
|
ReactDOM.render(<App />, app);
|
||||||
|
|
24
packages/ui-new/src/pages/Consent/index.tsx
Normal file
24
packages/ui-new/src/pages/Consent/index.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { consent } from '@/apis/consent';
|
||||||
|
import useApi from '@/hooks/use-api';
|
||||||
|
|
||||||
|
const Consent = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { result, run: asyncConsent } = useApi(consent);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void asyncConsent();
|
||||||
|
}, [asyncConsent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (result?.redirectTo) {
|
||||||
|
window.location.assign(result.redirectTo);
|
||||||
|
}
|
||||||
|
}, [result]);
|
||||||
|
|
||||||
|
return <div>{t('sign_in.loading')}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Consent;
|
50
packages/ui-new/src/pages/Register/index.module.scss
Normal file
50
packages/ui-new/src/pages/Register/index.module.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding: _.unit(8);
|
||||||
|
height: 100%;
|
||||||
|
@include _.flex-colomn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
@include _.flex-colomn;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-bottom: _.unit(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: var(--font-heading-1);
|
||||||
|
color: var(--color-heading);
|
||||||
|
margin-bottom: _.unit(9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
margin-bottom: _.unit(-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box,
|
||||||
|
> input:not([type='button']) {
|
||||||
|
margin-top: _.unit(3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input[type='button'] {
|
||||||
|
margin-top: _.unit(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.haveAccount {
|
||||||
|
position: absolute;
|
||||||
|
bottom: _.unit(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prefix {
|
||||||
|
font: var(--font-body-bold);
|
||||||
|
color: var(--color-placeholder);
|
||||||
|
margin-right: _.unit(0.5);
|
||||||
|
}
|
||||||
|
}
|
23
packages/ui-new/src/pages/Register/index.test.tsx
Normal file
23
packages/ui-new/src/pages/Register/index.test.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { register } from '@/apis/register';
|
||||||
|
import Register from '@/pages/Register';
|
||||||
|
|
||||||
|
jest.mock('@/apis/register', () => ({ register: jest.fn(async () => Promise.resolve()) }));
|
||||||
|
|
||||||
|
describe('<Register />', () => {
|
||||||
|
test('renders without exploding', async () => {
|
||||||
|
const { queryByText, getByText } = render(<Register />);
|
||||||
|
expect(queryByText('register.create_account')).not.toBeNull();
|
||||||
|
expect(queryByText('register.have_account')).not.toBeNull();
|
||||||
|
|
||||||
|
const submit = getByText('register.action');
|
||||||
|
fireEvent.click(submit);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(register).toBeCalled();
|
||||||
|
expect(queryByText('register.loading')).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
75
packages/ui-new/src/pages/Register/index.tsx
Normal file
75
packages/ui-new/src/pages/Register/index.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { LogtoErrorI18nKey } from '@logto/phrases';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { FC, FormEventHandler, useState, useCallback, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { register } from '@/apis/register';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Input from '@/components/Input';
|
||||||
|
import MessageBox from '@/components/MessageBox';
|
||||||
|
import TextLink from '@/components/TextLink';
|
||||||
|
import useApi from '@/hooks/use-api';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
const Register: FC = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const { loading, error, result, run: asyncRegister } = useApi(register);
|
||||||
|
|
||||||
|
const signUp: FormEventHandler = useCallback(
|
||||||
|
async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await asyncRegister(username, password);
|
||||||
|
},
|
||||||
|
[username, password, asyncRegister]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (result?.redirectTo) {
|
||||||
|
window.location.assign(result.redirectTo);
|
||||||
|
}
|
||||||
|
}, [result]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.wrapper)}>
|
||||||
|
<form className={classNames(styles.form)}>
|
||||||
|
<div className={styles.title}>{t('register.create_account')}</div>
|
||||||
|
<Input
|
||||||
|
name="username"
|
||||||
|
isDisabled={loading}
|
||||||
|
placeholder={t('sign_in.username')}
|
||||||
|
value={username}
|
||||||
|
onChange={setUsername} // TODO: account validation
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="password"
|
||||||
|
isDisabled={loading}
|
||||||
|
placeholder={t('sign_in.password')}
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={setPassword} // TODO: password validation
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<MessageBox className={styles.box}>
|
||||||
|
{i18n.t<string, LogtoErrorI18nKey>(`errors:${error.code}`)}
|
||||||
|
</MessageBox>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isDisabled={loading}
|
||||||
|
value={loading ? t('register.loading') : t('register.action')}
|
||||||
|
onClick={signUp}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.haveAccount}>
|
||||||
|
<span className={styles.prefix}>{t('register.have_account')}</span>
|
||||||
|
<TextLink href="/sign-in">{t('sign_in.action')}</TextLink>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Register;
|
51
packages/ui-new/src/pages/SignIn/index.module.scss
Normal file
51
packages/ui-new/src/pages/SignIn/index.module.scss
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
|
||||||
|
@mixin flex-colomn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding: _.unit(8);
|
||||||
|
height: 100%;
|
||||||
|
@include flex-colomn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
@include flex-colomn;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-bottom: _.unit(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: var(--font-heading-1);
|
||||||
|
color: var(--color-heading);
|
||||||
|
margin-bottom: _.unit(9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
margin-bottom: _.unit(-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box,
|
||||||
|
> input:not([type='button']) {
|
||||||
|
margin-top: _.unit(3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input[type='button'] {
|
||||||
|
margin-top: _.unit(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.createAccount {
|
||||||
|
position: absolute;
|
||||||
|
bottom: _.unit(10);
|
||||||
|
}
|
||||||
|
}
|
22
packages/ui-new/src/pages/SignIn/index.test.tsx
Normal file
22
packages/ui-new/src/pages/SignIn/index.test.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { signInBasic } from '@/apis/sign-in';
|
||||||
|
import SignIn from '@/pages/SignIn';
|
||||||
|
|
||||||
|
jest.mock('@/apis/sign-in', () => ({ signInBasic: jest.fn(async () => Promise.resolve()) }));
|
||||||
|
|
||||||
|
describe('<SignIn />', () => {
|
||||||
|
test('renders without exploding', async () => {
|
||||||
|
const { queryByText, getByText } = render(<SignIn />);
|
||||||
|
expect(queryByText('Sign in to Logto')).not.toBeNull();
|
||||||
|
|
||||||
|
const submit = getByText('sign_in.action');
|
||||||
|
fireEvent.click(submit);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(signInBasic).toBeCalled();
|
||||||
|
expect(queryByText('sign_in.loading')).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
76
packages/ui-new/src/pages/SignIn/index.tsx
Normal file
76
packages/ui-new/src/pages/SignIn/index.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { LogtoErrorI18nKey } from '@logto/phrases';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { FC, FormEventHandler, useState, useCallback, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { signInBasic } from '@/apis/sign-in';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Input from '@/components/Input';
|
||||||
|
import MessageBox from '@/components/MessageBox';
|
||||||
|
import TextLink from '@/components/TextLink';
|
||||||
|
import useApi from '@/hooks/use-api';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
const SignIn: FC = () => {
|
||||||
|
// TODO: Consider creating cross page data modal
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const { loading, error, result, run: asyncSignInBasic } = useApi(signInBasic);
|
||||||
|
|
||||||
|
const signInHandler: FormEventHandler = useCallback(
|
||||||
|
async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await asyncSignInBasic(username, password);
|
||||||
|
},
|
||||||
|
[username, password, asyncSignInBasic]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (result?.redirectTo) {
|
||||||
|
window.location.assign(result.redirectTo);
|
||||||
|
}
|
||||||
|
}, [result]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.wrapper)}>
|
||||||
|
<form className={classNames(styles.form)}>
|
||||||
|
<div className={styles.title}>Sign in to Logto</div>
|
||||||
|
<Input
|
||||||
|
name="username"
|
||||||
|
autoComplete="username"
|
||||||
|
isDisabled={loading}
|
||||||
|
placeholder={t('sign_in.username')}
|
||||||
|
value={username}
|
||||||
|
onChange={setUsername}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
isDisabled={loading}
|
||||||
|
placeholder={t('sign_in.password')}
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={setPassword}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<MessageBox className={styles.box}>
|
||||||
|
{i18n.t<string, LogtoErrorI18nKey>(`errors:${error.code}`)}
|
||||||
|
</MessageBox>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isDisabled={loading}
|
||||||
|
value={loading ? t('sign_in.loading') : t('sign_in.action')}
|
||||||
|
onClick={signInHandler}
|
||||||
|
/>
|
||||||
|
<TextLink className={styles.createAccount} href="/register">
|
||||||
|
{t('register.create_account')}
|
||||||
|
</TextLink>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignIn;
|
10
packages/ui-new/src/scss/_underscore.scss
Normal file
10
packages/ui-new/src/scss/_underscore.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
@function unit($factor: 1, $unit: 'px') {
|
||||||
|
@return #{$factor * 4}#{$unit};
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin flex-colomn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
14
packages/ui-new/src/scss/normalized.scss
Normal file
14
packages/ui-new/src/scss/normalized.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
|
@ -1,5 +1,12 @@
|
||||||
{
|
{
|
||||||
"extends": "@silverhand/ts-config-react/tsconfig.base",
|
"extends": "@silverhand/ts-config-react/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
]
|
]
|
||||||
|
|
|
@ -20,6 +20,8 @@ importers:
|
||||||
|
|
||||||
packages/console:
|
packages/console:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@logto/phrases': ^0.1.0
|
||||||
|
'@logto/schemas': ^0.1.0
|
||||||
'@parcel/core': ^2.3.1
|
'@parcel/core': ^2.3.1
|
||||||
'@parcel/transformer-sass': ^2.3.1
|
'@parcel/transformer-sass': ^2.3.1
|
||||||
'@silverhand/eslint-config': ^0.8.1
|
'@silverhand/eslint-config': ^0.8.1
|
||||||
|
@ -39,6 +41,8 @@ importers:
|
||||||
stylelint: ^13.13.1
|
stylelint: ^13.13.1
|
||||||
typescript: ^4.5.5
|
typescript: ^4.5.5
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@logto/phrases': link:../phrases
|
||||||
|
'@logto/schemas': link:../schemas
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
react-dom: 17.0.2_react@17.0.2
|
react-dom: 17.0.2_react@17.0.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -166,6 +170,7 @@ importers:
|
||||||
|
|
||||||
packages/phrases:
|
packages/phrases:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@logto/schemas': ^0.1.0
|
||||||
'@silverhand/eslint-config': ^0.8.1
|
'@silverhand/eslint-config': ^0.8.1
|
||||||
'@silverhand/essentials': ^1.1.4
|
'@silverhand/essentials': ^1.1.4
|
||||||
'@silverhand/ts-config': ^0.8.1
|
'@silverhand/ts-config': ^0.8.1
|
||||||
|
@ -174,6 +179,7 @@ importers:
|
||||||
prettier: ^2.3.2
|
prettier: ^2.3.2
|
||||||
typescript: ^4.5.5
|
typescript: ^4.5.5
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@logto/schemas': link:../schemas
|
||||||
'@silverhand/essentials': 1.1.4
|
'@silverhand/essentials': 1.1.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@silverhand/eslint-config': 0.8.1_b07be603d0ceb19daeedad1772e0f2c4
|
'@silverhand/eslint-config': 0.8.1_b07be603d0ceb19daeedad1772e0f2c4
|
||||||
|
@ -309,6 +315,8 @@ importers:
|
||||||
|
|
||||||
packages/ui-new:
|
packages/ui-new:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@logto/phrases': ^0.1.0
|
||||||
|
'@logto/schemas': ^0.1.0
|
||||||
'@parcel/core': ^2.3.1
|
'@parcel/core': ^2.3.1
|
||||||
'@parcel/transformer-sass': ^2.3.1
|
'@parcel/transformer-sass': ^2.3.1
|
||||||
'@silverhand/eslint-config': ^0.8.1
|
'@silverhand/eslint-config': ^0.8.1
|
||||||
|
@ -317,7 +325,12 @@ importers:
|
||||||
'@silverhand/ts-config-react': ^0.8.1
|
'@silverhand/ts-config-react': ^0.8.1
|
||||||
'@types/react': ^17.0.14
|
'@types/react': ^17.0.14
|
||||||
'@types/react-dom': ^17.0.9
|
'@types/react-dom': ^17.0.9
|
||||||
|
'@types/react-router-dom': ^5.3.2
|
||||||
|
classnames: ^2.3.1
|
||||||
eslint: ^8.1.0
|
eslint: ^8.1.0
|
||||||
|
i18next: ^21.6.11
|
||||||
|
i18next-browser-languagedetector: ^6.1.3
|
||||||
|
ky: ^0.29.0
|
||||||
lint-staged: ^11.1.1
|
lint-staged: ^11.1.1
|
||||||
parcel: ^2.3.1
|
parcel: ^2.3.1
|
||||||
postcss: ^8.4.6
|
postcss: ^8.4.6
|
||||||
|
@ -325,10 +338,20 @@ importers:
|
||||||
prettier: ^2.3.2
|
prettier: ^2.3.2
|
||||||
react: ^17.0.2
|
react: ^17.0.2
|
||||||
react-dom: ^17.0.2
|
react-dom: ^17.0.2
|
||||||
|
react-i18next: ^11.15.4
|
||||||
|
react-router-dom: ^5.2.0
|
||||||
stylelint: ^13.13.1
|
stylelint: ^13.13.1
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@logto/phrases': link:../phrases
|
||||||
|
'@logto/schemas': link:../schemas
|
||||||
|
classnames: 2.3.1
|
||||||
|
i18next: 21.6.11
|
||||||
|
i18next-browser-languagedetector: 6.1.3
|
||||||
|
ky: 0.29.0
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
react-dom: 17.0.2_react@17.0.2
|
react-dom: 17.0.2_react@17.0.2
|
||||||
|
react-i18next: 11.15.4_3fb644aa30122a07f960d67fa51d6dc1
|
||||||
|
react-router-dom: 5.3.0_react@17.0.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@parcel/core': 2.3.1
|
'@parcel/core': 2.3.1
|
||||||
'@parcel/transformer-sass': 2.3.1_@parcel+core@2.3.1
|
'@parcel/transformer-sass': 2.3.1_@parcel+core@2.3.1
|
||||||
|
@ -338,6 +361,7 @@ importers:
|
||||||
'@silverhand/ts-config-react': 0.8.1_typescript@4.5.5
|
'@silverhand/ts-config-react': 0.8.1_typescript@4.5.5
|
||||||
'@types/react': 17.0.37
|
'@types/react': 17.0.37
|
||||||
'@types/react-dom': 17.0.11
|
'@types/react-dom': 17.0.11
|
||||||
|
'@types/react-router-dom': 5.3.2
|
||||||
eslint: 8.4.1
|
eslint: 8.4.1
|
||||||
lint-staged: 11.2.6
|
lint-staged: 11.2.6
|
||||||
parcel: 2.3.1_postcss@8.4.6
|
parcel: 2.3.1_postcss@8.4.6
|
||||||
|
@ -9248,12 +9272,24 @@ packages:
|
||||||
'@babel/runtime': 7.16.3
|
'@babel/runtime': 7.16.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/i18next-browser-languagedetector/6.1.3:
|
||||||
|
resolution: {integrity: sha512-T+oGXHXtrur14CGnZZ7qQ07X38XJQEI00b/4ILrtO6xPbwTlQ1wtMZC2H+tBULixHuVUXv8LKbxfjyITJkezUg==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.16.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/i18next/20.6.1:
|
/i18next/20.6.1:
|
||||||
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
|
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.16.3
|
'@babel/runtime': 7.16.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/i18next/21.6.11:
|
||||||
|
resolution: {integrity: sha512-tJ2+o0lVO+fhi8bPkCpBAeY1SgkqmQm5NzgPWCQssBrywJw98/o+Kombhty5nxQOpHtvMmsxcOopczUiH6bJxQ==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.16.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/iconv-lite/0.4.24:
|
/iconv-lite/0.4.24:
|
||||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -11324,6 +11360,11 @@ packages:
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/ky/0.29.0:
|
||||||
|
resolution: {integrity: sha512-01TBSOqlHmLfcQhHseugGHLxPtU03OyZWaLDWt5MfzCkijG6xWFvAQPhKVn0cR2MMjYvBP9keQ8A3+rQEhLO5g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lerna/4.0.0:
|
/lerna/4.0.0:
|
||||||
resolution: {integrity: sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg==}
|
resolution: {integrity: sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg==}
|
||||||
engines: {node: '>= 10.18.0'}
|
engines: {node: '>= 10.18.0'}
|
||||||
|
@ -14817,6 +14858,27 @@ packages:
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-i18next/11.15.4_3fb644aa30122a07f960d67fa51d6dc1:
|
||||||
|
resolution: {integrity: sha512-jKJNAcVcbPGK+yrTcXhLblgPY16n6NbpZZL3Mk8nswj1v3ayIiUBVDU09SgqnT+DluyQBS97hwSvPU5yVFG0yg==}
|
||||||
|
peerDependencies:
|
||||||
|
i18next: '>= 19.0.0'
|
||||||
|
react: '>= 16.8.0'
|
||||||
|
react-dom: '*'
|
||||||
|
react-native: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-native:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.16.3
|
||||||
|
html-escaper: 2.0.2
|
||||||
|
html-parse-stringify: 3.0.1
|
||||||
|
i18next: 21.6.11
|
||||||
|
react: 17.0.2
|
||||||
|
react-dom: 17.0.2_react@17.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-is/16.13.1:
|
/react-is/16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue