mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(demo-app): implement (part 2)
* dark mode * i18n * sign out * fetch userinfo
This commit is contained in:
parent
88e2120e25
commit
85a055efa4
11 changed files with 146 additions and 51 deletions
|
@ -1,5 +1,5 @@
|
||||||
@use '@/scss/underscore' as _;
|
@use '@/scss/underscore' as _;
|
||||||
@use '@/scss/fonts';
|
@use '@logto/shared/scss/fonts';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"start": "parcel src/index.html",
|
"start": "parcel src/index.html",
|
||||||
"dev": "PORT=5003 parcel src/index.html --public-url /demo-app --no-cache --hmr-port 6003",
|
"dev": "cross-env PORT=5003 parcel src/index.html --public-url /demo-app --no-cache --hmr-port 6003",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url /demo-app",
|
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url /demo-app",
|
||||||
"lint": "eslint --ext .ts --ext .tsx src",
|
"lint": "eslint --ext .ts --ext .tsx src",
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
"stylelint": "stylelint \"src/**/*.scss\""
|
"stylelint": "stylelint \"src/**/*.scss\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@logto/phrases": "^0.1.0",
|
||||||
"@logto/react": "^0.1.14",
|
"@logto/react": "^0.1.14",
|
||||||
"@logto/schemas": "^0.1.0",
|
"@logto/schemas": "^0.1.0",
|
||||||
"@logto/shared": "^0.1.0",
|
"@logto/shared": "^0.1.0",
|
||||||
|
@ -28,13 +29,17 @@
|
||||||
"@silverhand/ts-config-react": "^0.14.0",
|
"@silverhand/ts-config-react": "^0.14.0",
|
||||||
"@types/react": "^17.0.14",
|
"@types/react": "^17.0.14",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
|
"i18next": "^21.6.12",
|
||||||
|
"i18next-browser-languagedetector": "^6.1.3",
|
||||||
"lint-staged": "^13.0.0",
|
"lint-staged": "^13.0.0",
|
||||||
"parcel": "2.5.0",
|
"parcel": "2.5.0",
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.6",
|
||||||
"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",
|
||||||
"stylelint": "^14.8.2",
|
"stylelint": "^14.8.2",
|
||||||
"typescript": "^4.7.2"
|
"typescript": "^4.7.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
@use '@/scss/underscore' as _;
|
@use '@/scss/underscore' as _;
|
||||||
|
@use '@logto/shared/scss/fonts';
|
||||||
@use '@logto/shared/scss/console-themes' as themes;
|
@use '@logto/shared/scss/console-themes' as themes;
|
||||||
|
|
||||||
body {
|
|
||||||
background: #edebf6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
@include themes.light;
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--color-background);
|
background: var(--color-layer-1);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -28,27 +23,30 @@ body {
|
||||||
.title {
|
.title {
|
||||||
margin-top: _.unit(6);
|
margin-top: _.unit(6);
|
||||||
color: var(--color-neutral-10);
|
color: var(--color-neutral-10);
|
||||||
font-weight: 600;
|
font: var(--font-title-medium);
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin-top: _.unit(1);
|
margin-top: _.unit(1);
|
||||||
|
font: var(--font-body-medium);
|
||||||
color: var(--color-caption);
|
color: var(--color-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoCard {
|
.infoCard {
|
||||||
margin-top: _.unit(4);
|
margin-top: _.unit(4);
|
||||||
padding: _.unit(1.5) _.unit(4);
|
padding: _.unit(4);
|
||||||
|
font: var(--font-body-medium);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-layer-2);
|
background: var(--color-layer-2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font: var(--font-label-large);
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
div + div {
|
||||||
margin: _.unit(2.5) 0;
|
margin-top: _.unit(2.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +56,7 @@ body {
|
||||||
border: 1px solid var(--color-outline);
|
border: 1px solid var(--color-outline);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: _.unit(3) _.unit(6);
|
padding: _.unit(3) _.unit(6);
|
||||||
font-weight: 500;
|
font: var(--font-button);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
transition: background ease-in-out 0.2s;
|
transition: background ease-in-out 0.2s;
|
||||||
|
|
||||||
|
@ -107,3 +105,15 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
body {
|
||||||
|
@include themes.light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
@include themes.dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,36 +1,44 @@
|
||||||
import { LogtoProvider, useLogto } from '@logto/react';
|
import { LogtoProvider, useLogto, UserInfoResponse } from '@logto/react';
|
||||||
import { signInNotificationStorageKey } from '@logto/schemas';
|
import { signInNotificationStorageKey } from '@logto/schemas';
|
||||||
import { demoAppApplicationId } from '@logto/schemas/lib/seeds';
|
import { demoAppApplicationId } from '@logto/schemas/lib/seeds';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import '@/scss/normalized.scss';
|
import '@/scss/normalized.scss';
|
||||||
import * as styles from './App.module.scss';
|
import * as styles from './App.module.scss';
|
||||||
import Callback from './Callback';
|
import Callback from './Callback';
|
||||||
import congrats from './assets/congrats.svg';
|
import congrats from './assets/congrats.svg';
|
||||||
|
import initI18n from './i18n/init';
|
||||||
|
|
||||||
|
void initI18n();
|
||||||
|
|
||||||
const Main = () => {
|
const Main = () => {
|
||||||
const { isAuthenticated, signIn } = useLogto();
|
const { isAuthenticated, fetchUserInfo, signIn, signOut } = useLogto();
|
||||||
|
const [user, setUser] = useState<UserInfoResponse>();
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'demo_app' });
|
||||||
const isInCallback = Boolean(new URL(window.location.href).searchParams.get('code'));
|
const isInCallback = Boolean(new URL(window.location.href).searchParams.get('code'));
|
||||||
|
|
||||||
// Pending SDK fix
|
|
||||||
const username = 'foo';
|
|
||||||
const userId = 'bar';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated && !isInCallback) {
|
if (!isAuthenticated && !isInCallback) {
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(signInNotificationStorageKey, t('notification'));
|
||||||
signInNotificationStorageKey,
|
|
||||||
'Use the admin username and password to sign in this demo.'
|
|
||||||
);
|
|
||||||
void signIn(window.location.href);
|
void signIn(window.location.href);
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, isInCallback, signIn]);
|
}, [isAuthenticated, isInCallback, signIn, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const userInfo = await fetchUserInfo();
|
||||||
|
setUser(userInfo);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [isAuthenticated, fetchUserInfo]);
|
||||||
|
|
||||||
if (isInCallback) {
|
if (isInCallback) {
|
||||||
return <Callback />;
|
return <Callback />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated || !user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,29 +46,35 @@ const Main = () => {
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<img src={congrats} alt="Congrats" />
|
<img src={congrats} alt="Congrats" />
|
||||||
<div className={styles.title}>You've successfully signed in the demo app!</div>
|
<div className={styles.title}>{t('title')}</div>
|
||||||
<div className={styles.text}>Here is your personal information:</div>
|
<div className={styles.text}>{t('subtitle')}</div>
|
||||||
<div className={styles.infoCard}>
|
<div className={styles.infoCard}>
|
||||||
<p>
|
<div>
|
||||||
Username: <b>{username}</b>
|
{t('username')}
|
||||||
</p>
|
<span>{user.username}</span>
|
||||||
<p>
|
</div>
|
||||||
User ID: <b>{userId}</b>
|
<div>
|
||||||
</p>
|
{t('user_id')}
|
||||||
|
<span>{user.sub}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles.button}
|
||||||
|
onClick={async () => signOut(`${window.location.origin}/demo-app`)}
|
||||||
|
>
|
||||||
|
{t('sign_out')}
|
||||||
</div>
|
</div>
|
||||||
{/* Pending SDK fix */}
|
|
||||||
<div className={styles.button}>Sign out the demo app</div>
|
|
||||||
<div className={styles.continue}>
|
<div className={styles.continue}>
|
||||||
<div className={styles.hr} />
|
<div className={styles.hr} />
|
||||||
or continue to explore
|
{t('continue_explore')}
|
||||||
<div className={styles.hr} />
|
<div className={styles.hr} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<a href="#">Customize sign-in experience</a>
|
<a href="#">{t('customize_sign_in_experience')}</a>
|
||||||
<span />
|
<span />
|
||||||
<a href="#">Enable passwordless</a>
|
<a href="#">{t('enable_passwordless')}</a>
|
||||||
<span />
|
<span />
|
||||||
<a href="#">Add a social connector</a>
|
<a href="#">{t('add_social_connector')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,14 +2,7 @@ import { useHandleSignInCallback } from '@logto/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Callback = () => {
|
const Callback = () => {
|
||||||
const { error, isAuthenticated } = useHandleSignInCallback('.');
|
const { error } = useHandleSignInCallback('demo-app');
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
const { href } = window.location;
|
|
||||||
window.location.assign(href.slice(0, href.indexOf('?')));
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|
19
packages/demo-app/src/i18n/init.ts
Normal file
19
packages/demo-app/src/i18n/init.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import resources, { Language } from '@logto/phrases';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
const initI18n = async (language?: Language) =>
|
||||||
|
i18next
|
||||||
|
.use(initReactI18next)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
lng: language,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default initI18n;
|
|
@ -2,6 +2,7 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
background: var(--color-base);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: auto;
|
-moz-osx-font-smoothing: auto;
|
||||||
}
|
}
|
||||||
|
@ -16,3 +17,22 @@ input {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar:horizontal {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-neutral-variant-80);
|
||||||
|
background-clip: content-box;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
|
@ -549,6 +549,18 @@ const translation = {
|
||||||
button: 'Sign in again',
|
button: 'Sign in again',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification: 'Use the admin username and password to sign in this demo.',
|
||||||
|
title: "You've successfully signed in the demo app!",
|
||||||
|
subtitle: 'Here is your personal information:',
|
||||||
|
username: 'Username: ',
|
||||||
|
user_id: 'User ID: ',
|
||||||
|
sign_out: 'Sign out the demo app',
|
||||||
|
continue_explore: 'Or continue to explore',
|
||||||
|
customize_sign_in_experience: 'Customize sign-in experience',
|
||||||
|
enable_passwordless: 'Enable passwordless',
|
||||||
|
add_social_connector: 'Add a social connector',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = {
|
const errors = {
|
||||||
|
|
|
@ -533,6 +533,18 @@ const translation = {
|
||||||
button: '重新登录',
|
button: '重新登录',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
demo_app: {
|
||||||
|
notification: '请使用管理员账号密码登录本示例应用',
|
||||||
|
title: '恭喜!你已成功登录到示例应用!',
|
||||||
|
subtitle: '以下是你本次登录的用户信息:',
|
||||||
|
username: '用户名:',
|
||||||
|
user_id: '用户 ID:',
|
||||||
|
sign_out: '登出',
|
||||||
|
continue_explore: '或继续探索',
|
||||||
|
customize_sign_in_experience: '自定义登录体验',
|
||||||
|
enable_passwordless: '启用无密码登录',
|
||||||
|
add_social_connector: '添加社会化连接器',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const errors = {
|
const errors = {
|
||||||
|
|
|
@ -900,6 +900,7 @@ importers:
|
||||||
|
|
||||||
packages/demo-app:
|
packages/demo-app:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@logto/phrases': ^0.1.0
|
||||||
'@logto/react': ^0.1.14
|
'@logto/react': ^0.1.14
|
||||||
'@logto/schemas': ^0.1.0
|
'@logto/schemas': ^0.1.0
|
||||||
'@logto/shared': ^0.1.0
|
'@logto/shared': ^0.1.0
|
||||||
|
@ -911,16 +912,21 @@ importers:
|
||||||
'@silverhand/ts-config-react': ^0.14.0
|
'@silverhand/ts-config-react': ^0.14.0
|
||||||
'@types/react': ^17.0.14
|
'@types/react': ^17.0.14
|
||||||
'@types/react-dom': ^17.0.9
|
'@types/react-dom': ^17.0.9
|
||||||
|
cross-env: ^7.0.3
|
||||||
eslint: ^8.10.0
|
eslint: ^8.10.0
|
||||||
|
i18next: ^21.6.12
|
||||||
|
i18next-browser-languagedetector: ^6.1.3
|
||||||
lint-staged: ^13.0.0
|
lint-staged: ^13.0.0
|
||||||
parcel: 2.5.0
|
parcel: 2.5.0
|
||||||
postcss: ^8.4.6
|
postcss: ^8.4.6
|
||||||
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
|
||||||
stylelint: ^14.8.2
|
stylelint: ^14.8.2
|
||||||
typescript: ^4.7.2
|
typescript: ^4.7.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@logto/phrases': link:../phrases
|
||||||
'@logto/react': 0.1.14_react@17.0.2
|
'@logto/react': 0.1.14_react@17.0.2
|
||||||
'@logto/schemas': link:../schemas
|
'@logto/schemas': link:../schemas
|
||||||
'@logto/shared': link:../shared
|
'@logto/shared': link:../shared
|
||||||
|
@ -932,13 +938,17 @@ importers:
|
||||||
'@silverhand/ts-config-react': 0.14.0_typescript@4.7.2
|
'@silverhand/ts-config-react': 0.14.0_typescript@4.7.2
|
||||||
'@types/react': 17.0.37
|
'@types/react': 17.0.37
|
||||||
'@types/react-dom': 17.0.11
|
'@types/react-dom': 17.0.11
|
||||||
|
cross-env: 7.0.3
|
||||||
eslint: 8.10.0
|
eslint: 8.10.0
|
||||||
|
i18next: 21.6.12
|
||||||
|
i18next-browser-languagedetector: 6.1.3
|
||||||
lint-staged: 13.0.0
|
lint-staged: 13.0.0
|
||||||
parcel: 2.5.0_postcss@8.4.14
|
parcel: 2.5.0_postcss@8.4.14
|
||||||
postcss: 8.4.14
|
postcss: 8.4.14
|
||||||
prettier: 2.5.1
|
prettier: 2.5.1
|
||||||
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_fq32mavcto3l2u7t3zyhvdh4yu
|
||||||
stylelint: 14.8.2
|
stylelint: 14.8.2
|
||||||
typescript: 4.7.2
|
typescript: 4.7.2
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue