0
Fork 0
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:
Charles Zhao 2022-06-06 20:06:34 +08:00
parent 88e2120e25
commit 85a055efa4
No known key found for this signature in database
GPG key ID: 4858774754C92DF2
11 changed files with 146 additions and 51 deletions

View file

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

View file

@ -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"
}, },

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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 = {

View file

@ -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 = {

View file

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