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/fonts';
|
||||
@use '@logto/shared/scss/fonts';
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"precommit": "lint-staged",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -17,6 +17,7 @@
|
|||
"stylelint": "stylelint \"src/**/*.scss\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/phrases": "^0.1.0",
|
||||
"@logto/react": "^0.1.14",
|
||||
"@logto/schemas": "^0.1.0",
|
||||
"@logto/shared": "^0.1.0",
|
||||
|
@ -28,13 +29,17 @@
|
|||
"@silverhand/ts-config-react": "^0.14.0",
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.10.0",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"lint-staged": "^13.0.0",
|
||||
"parcel": "2.5.0",
|
||||
"postcss": "^8.4.6",
|
||||
"prettier": "^2.3.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.4",
|
||||
"stylelint": "^14.8.2",
|
||||
"typescript": "^4.7.2"
|
||||
},
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
@use '@logto/shared/scss/fonts';
|
||||
@use '@logto/shared/scss/console-themes' as themes;
|
||||
|
||||
body {
|
||||
background: #edebf6;
|
||||
}
|
||||
|
||||
.app {
|
||||
@include themes.light;
|
||||
|
||||
.card {
|
||||
background: var(--color-background);
|
||||
background: var(--color-layer-1);
|
||||
border-radius: 16px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
|
@ -28,27 +23,30 @@ body {
|
|||
.title {
|
||||
margin-top: _.unit(6);
|
||||
color: var(--color-neutral-10);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font: var(--font-title-medium);
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: _.unit(1);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-caption);
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
margin-top: _.unit(4);
|
||||
padding: _.unit(1.5) _.unit(4);
|
||||
padding: _.unit(4);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text);
|
||||
background: var(--color-layer-2);
|
||||
border-radius: 8px;
|
||||
width: 400px;
|
||||
|
||||
span {
|
||||
font: var(--font-label-large);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: _.unit(2.5) 0;
|
||||
div + div {
|
||||
margin-top: _.unit(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +56,7 @@ body {
|
|||
border: 1px solid var(--color-outline);
|
||||
border-radius: 8px;
|
||||
padding: _.unit(3) _.unit(6);
|
||||
font-weight: 500;
|
||||
font: var(--font-button);
|
||||
color: var(--color-text);
|
||||
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 { 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 * as styles from './App.module.scss';
|
||||
import Callback from './Callback';
|
||||
import congrats from './assets/congrats.svg';
|
||||
import initI18n from './i18n/init';
|
||||
|
||||
void initI18n();
|
||||
|
||||
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'));
|
||||
|
||||
// Pending SDK fix
|
||||
const username = 'foo';
|
||||
const userId = 'bar';
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated && !isInCallback) {
|
||||
sessionStorage.setItem(
|
||||
signInNotificationStorageKey,
|
||||
'Use the admin username and password to sign in this demo.'
|
||||
);
|
||||
sessionStorage.setItem(signInNotificationStorageKey, t('notification'));
|
||||
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) {
|
||||
return <Callback />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
if (!isAuthenticated || !user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -38,29 +46,35 @@ const Main = () => {
|
|||
<div className={styles.app}>
|
||||
<div className={styles.card}>
|
||||
<img src={congrats} alt="Congrats" />
|
||||
<div className={styles.title}>You've successfully signed in the demo app!</div>
|
||||
<div className={styles.text}>Here is your personal information:</div>
|
||||
<div className={styles.title}>{t('title')}</div>
|
||||
<div className={styles.text}>{t('subtitle')}</div>
|
||||
<div className={styles.infoCard}>
|
||||
<p>
|
||||
Username: <b>{username}</b>
|
||||
</p>
|
||||
<p>
|
||||
User ID: <b>{userId}</b>
|
||||
</p>
|
||||
<div>
|
||||
{t('username')}
|
||||
<span>{user.username}</span>
|
||||
</div>
|
||||
<div>
|
||||
{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>
|
||||
{/* Pending SDK fix */}
|
||||
<div className={styles.button}>Sign out the demo app</div>
|
||||
<div className={styles.continue}>
|
||||
<div className={styles.hr} />
|
||||
or continue to explore
|
||||
{t('continue_explore')}
|
||||
<div className={styles.hr} />
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<a href="#">Customize sign-in experience</a>
|
||||
<a href="#">{t('customize_sign_in_experience')}</a>
|
||||
<span />
|
||||
<a href="#">Enable passwordless</a>
|
||||
<a href="#">{t('enable_passwordless')}</a>
|
||||
<span />
|
||||
<a href="#">Add a social connector</a>
|
||||
<a href="#">{t('add_social_connector')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,14 +2,7 @@ import { useHandleSignInCallback } from '@logto/react';
|
|||
import React from 'react';
|
||||
|
||||
const Callback = () => {
|
||||
const { error, isAuthenticated } = useHandleSignInCallback('.');
|
||||
|
||||
if (isAuthenticated) {
|
||||
const { href } = window.location;
|
||||
window.location.assign(href.slice(0, href.indexOf('?')));
|
||||
|
||||
return null;
|
||||
}
|
||||
const { error } = useHandleSignInCallback('demo-app');
|
||||
|
||||
if (error) {
|
||||
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;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
background: var(--color-base);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: auto;
|
||||
}
|
||||
|
@ -16,3 +17,22 @@ input {
|
|||
border: 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',
|
||||
},
|
||||
},
|
||||
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 = {
|
||||
|
|
|
@ -533,6 +533,18 @@ const translation = {
|
|||
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 = {
|
||||
|
|
|
@ -900,6 +900,7 @@ importers:
|
|||
|
||||
packages/demo-app:
|
||||
specifiers:
|
||||
'@logto/phrases': ^0.1.0
|
||||
'@logto/react': ^0.1.14
|
||||
'@logto/schemas': ^0.1.0
|
||||
'@logto/shared': ^0.1.0
|
||||
|
@ -911,16 +912,21 @@ importers:
|
|||
'@silverhand/ts-config-react': ^0.14.0
|
||||
'@types/react': ^17.0.14
|
||||
'@types/react-dom': ^17.0.9
|
||||
cross-env: ^7.0.3
|
||||
eslint: ^8.10.0
|
||||
i18next: ^21.6.12
|
||||
i18next-browser-languagedetector: ^6.1.3
|
||||
lint-staged: ^13.0.0
|
||||
parcel: 2.5.0
|
||||
postcss: ^8.4.6
|
||||
prettier: ^2.3.2
|
||||
react: ^17.0.2
|
||||
react-dom: ^17.0.2
|
||||
react-i18next: ^11.15.4
|
||||
stylelint: ^14.8.2
|
||||
typescript: ^4.7.2
|
||||
devDependencies:
|
||||
'@logto/phrases': link:../phrases
|
||||
'@logto/react': 0.1.14_react@17.0.2
|
||||
'@logto/schemas': link:../schemas
|
||||
'@logto/shared': link:../shared
|
||||
|
@ -932,13 +938,17 @@ importers:
|
|||
'@silverhand/ts-config-react': 0.14.0_typescript@4.7.2
|
||||
'@types/react': 17.0.37
|
||||
'@types/react-dom': 17.0.11
|
||||
cross-env: 7.0.3
|
||||
eslint: 8.10.0
|
||||
i18next: 21.6.12
|
||||
i18next-browser-languagedetector: 6.1.3
|
||||
lint-staged: 13.0.0
|
||||
parcel: 2.5.0_postcss@8.4.14
|
||||
postcss: 8.4.14
|
||||
prettier: 2.5.1
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
react-i18next: 11.15.4_fq32mavcto3l2u7t3zyhvdh4yu
|
||||
stylelint: 14.8.2
|
||||
typescript: 4.7.2
|
||||
|
||||
|
|
Loading…
Reference in a new issue