0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

refactor(console): hide check demo button if demo app is deleted

This commit is contained in:
Charles Zhao 2022-06-07 11:51:26 +08:00
parent 69e32cb646
commit fbd7ac3a69
No known key found for this signature in database
GPG key ID: 4858774754C92DF2
6 changed files with 116 additions and 31 deletions

View file

@ -1,17 +1,18 @@
import { useLogto } from '@logto/react'; import { useLogto } from '@logto/react';
import { RequestErrorBody } from '@logto/schemas'; import { RequestErrorBody } from '@logto/schemas';
import { managementResource } from '@logto/schemas/lib/seeds'; import { managementResource } from '@logto/schemas/lib/seeds';
import { conditional } from '@silverhand/essentials';
import { t } from 'i18next'; import { t } from 'i18next';
import ky from 'ky'; import ky from 'ky';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
export class RequestError extends Error { export class RequestError extends Error {
status: number;
body?: RequestErrorBody; body?: RequestErrorBody;
constructor(body: RequestErrorBody) { constructor(status: number, body: RequestErrorBody) {
super('Request error occurred.'); super('Request error occurred.');
this.status = status;
this.body = body; this.body = body;
} }
} }
@ -36,17 +37,15 @@ const useApi = ({ hideErrorToast }: Props = {}) => {
() => () =>
ky.create({ ky.create({
hooks: { hooks: {
beforeError: conditional( beforeError: hideErrorToast
!hideErrorToast && [ ? []
(error) => { : [
const { response } = error; (error) => {
void toastError(error.response);
void toastError(response); return error;
},
return error; ],
},
]
),
beforeRequest: [ beforeRequest: [
async (request) => { async (request) => {
if (isAuthenticated) { if (isAuthenticated) {

View file

@ -41,7 +41,7 @@ const useSwrFetcher: useSwrFetcherHook = <T>() => {
if (error instanceof HTTPError) { if (error instanceof HTTPError) {
const { response } = error; const { response } = error;
const metadata = await response.json<RequestErrorBody>(); const metadata = await response.json<RequestErrorBody>();
throw new RequestError(metadata); throw new RequestError(response.status, metadata);
} }
throw error; throw error;
} }

View file

@ -0,0 +1,44 @@
@use '@/scss/underscore' as _;
.card {
display: flex;
padding: _.unit(6) _.unit(8);
background-color: var(--color-layer-1);
border-radius: 16px;
.icon {
@include _.shimmering-animation;
width: 48px;
height: 48px;
margin-right: _.unit(6);
}
.wrapper {
flex: 1;
display: flex;
flex-direction: column;
.title {
@include _.shimmering-animation;
width: 113px;
height: 20px;
}
.subtitle {
@include _.shimmering-animation;
width: 453px;
height: 20px;
margin-top: _.unit(1);
}
}
.button {
@include _.shimmering-animation;
width: 129px;
height: 44px;
}
}
.card + .card {
margin-top: _.unit(4);
}

View file

@ -0,0 +1,19 @@
import React from 'react';
import * as styles from './index.module.scss';
const Skeleton = () => (
<>
{[...Array.from({ length: 5 }).keys()].map((key) => (
<div key={key} className={styles.card}>
<div className={styles.icon} />
<div className={styles.wrapper}>
<div className={styles.title} />
<div className={styles.subtitle} />
</div>
</div>
))}
</>
);
export default Skeleton;

View file

@ -1,5 +1,7 @@
import { AdminConsoleKey, I18nKey } from '@logto/phrases'; import { AdminConsoleKey, I18nKey } from '@logto/phrases';
import { Application } from '@logto/schemas';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import useSWR from 'swr';
import checkDemoIcon from '@/assets/images/check-demo.svg'; import checkDemoIcon from '@/assets/images/check-demo.svg';
import createAppIcon from '@/assets/images/create-app.svg'; import createAppIcon from '@/assets/images/create-app.svg';
@ -7,6 +9,7 @@ import customizeIcon from '@/assets/images/customize.svg';
import furtherReadingsIcon from '@/assets/images/further-readings.svg'; import furtherReadingsIcon from '@/assets/images/further-readings.svg';
import oneClickIcon from '@/assets/images/one-click.svg'; import oneClickIcon from '@/assets/images/one-click.svg';
import passwordlessIcon from '@/assets/images/passwordless.svg'; import passwordlessIcon from '@/assets/images/passwordless.svg';
import { RequestError } from '@/hooks/use-api';
import useSettings from '@/hooks/use-settings'; import useSettings from '@/hooks/use-settings';
type GetStartedMetadata = { type GetStartedMetadata = {
@ -16,12 +19,24 @@ type GetStartedMetadata = {
icon: string; icon: string;
buttonText: I18nKey; buttonText: I18nKey;
isComplete?: boolean; isComplete?: boolean;
isHidden?: boolean;
onClick: () => void; onClick: () => void;
}; };
const useGetStartedMetadata = () => { const useGetStartedMetadata = () => {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const { data: demoApp, error } = useSWR<Application, RequestError>('/api/applications/demo_app', {
shouldRetryOnError: (error: unknown) => {
if (error instanceof RequestError) {
return error.status !== 404;
}
return true;
},
});
const navigate = useNavigate(); const navigate = useNavigate();
const isLoadingDemoApp = !demoApp && !error;
const hideDemo = error?.status === 404;
const data: GetStartedMetadata[] = [ const data: GetStartedMetadata[] = [
{ {
@ -31,6 +46,7 @@ const useGetStartedMetadata = () => {
icon: checkDemoIcon, icon: checkDemoIcon,
buttonText: 'general.check_out', buttonText: 'general.check_out',
isComplete: settings?.checkDemo, isComplete: settings?.checkDemo,
isHidden: hideDemo,
onClick: async () => { onClick: async () => {
void updateSettings({ checkDemo: true }); void updateSettings({ checkDemo: true });
window.open('/demo-app', '_blank'); window.open('/demo-app', '_blank');
@ -97,6 +113,7 @@ const useGetStartedMetadata = () => {
data, data,
completedCount: data.filter(({ isComplete }) => isComplete).length, completedCount: data.filter(({ isComplete }) => isComplete).length,
totalCount: data.length, totalCount: data.length,
isLoading: isLoadingDemoApp,
}; };
}; };

View file

@ -9,13 +9,14 @@ import ConfirmModal from '@/components/ConfirmModal';
import Spacer from '@/components/Spacer'; import Spacer from '@/components/Spacer';
import useUserPreferences from '@/hooks/use-user-preferences'; import useUserPreferences from '@/hooks/use-user-preferences';
import Skeleton from './components/Skeleton';
import useGetStartedMetadata from './hook'; import useGetStartedMetadata from './hook';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const GetStarted = () => { const GetStarted = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const navigate = useNavigate(); const navigate = useNavigate();
const { data } = useGetStartedMetadata(); const { data, isLoading } = useGetStartedMetadata();
const { update } = useUserPreferences(); const { update } = useUserPreferences();
const [showConfirmModal, setShowConfirmModal] = useState(false); const [showConfirmModal, setShowConfirmModal] = useState(false);
@ -45,23 +46,28 @@ const GetStarted = () => {
</span> </span>
</div> </div>
</div> </div>
{data.map(({ id, title, subtitle, icon, isComplete, buttonText, onClick }) => ( {isLoading && <Skeleton />}
<Card key={id} className={styles.card}> {!isLoading &&
<img className={styles.icon} src={icon} /> data.map(
{isComplete && <img className={styles.completeIndicator} src={completeIndicator} />} ({ id, title, subtitle, icon, isComplete, isHidden, buttonText, onClick }) =>
<div className={styles.wrapper}> !isHidden && (
<div className={styles.title}>{t(title)}</div> <Card key={id} className={styles.card}>
<div className={styles.subtitle}>{t(subtitle)}</div> <img className={styles.icon} src={icon} />
</div> {isComplete && <img className={styles.completeIndicator} src={completeIndicator} />}
<Button <div className={styles.wrapper}>
className={styles.button} <div className={styles.title}>{t(title)}</div>
type="outline" <div className={styles.subtitle}>{t(subtitle)}</div>
size="large" </div>
title={buttonText} <Button
onClick={onClick} className={styles.button}
/> type="outline"
</Card> size="large"
))} title={buttonText}
onClick={onClick}
/>
</Card>
)
)}
<ConfirmModal <ConfirmModal
title="get_started.confirm" title="get_started.confirm"
isOpen={showConfirmModal} isOpen={showConfirmModal}