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:
parent
69e32cb646
commit
fbd7ac3a69
6 changed files with 116 additions and 31 deletions
|
@ -1,17 +1,18 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import { RequestErrorBody } from '@logto/schemas';
|
||||
import { managementResource } from '@logto/schemas/lib/seeds';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { t } from 'i18next';
|
||||
import ky from 'ky';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export class RequestError extends Error {
|
||||
status: number;
|
||||
body?: RequestErrorBody;
|
||||
|
||||
constructor(body: RequestErrorBody) {
|
||||
constructor(status: number, body: RequestErrorBody) {
|
||||
super('Request error occurred.');
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
@ -36,17 +37,15 @@ const useApi = ({ hideErrorToast }: Props = {}) => {
|
|||
() =>
|
||||
ky.create({
|
||||
hooks: {
|
||||
beforeError: conditional(
|
||||
!hideErrorToast && [
|
||||
(error) => {
|
||||
const { response } = error;
|
||||
beforeError: hideErrorToast
|
||||
? []
|
||||
: [
|
||||
(error) => {
|
||||
void toastError(error.response);
|
||||
|
||||
void toastError(response);
|
||||
|
||||
return error;
|
||||
},
|
||||
]
|
||||
),
|
||||
return error;
|
||||
},
|
||||
],
|
||||
beforeRequest: [
|
||||
async (request) => {
|
||||
if (isAuthenticated) {
|
||||
|
|
|
@ -41,7 +41,7 @@ const useSwrFetcher: useSwrFetcherHook = <T>() => {
|
|||
if (error instanceof HTTPError) {
|
||||
const { response } = error;
|
||||
const metadata = await response.json<RequestErrorBody>();
|
||||
throw new RequestError(metadata);
|
||||
throw new RequestError(response.status, metadata);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -1,5 +1,7 @@
|
|||
import { AdminConsoleKey, I18nKey } from '@logto/phrases';
|
||||
import { Application } from '@logto/schemas';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import checkDemoIcon from '@/assets/images/check-demo.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 oneClickIcon from '@/assets/images/one-click.svg';
|
||||
import passwordlessIcon from '@/assets/images/passwordless.svg';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
|
||||
type GetStartedMetadata = {
|
||||
|
@ -16,12 +19,24 @@ type GetStartedMetadata = {
|
|||
icon: string;
|
||||
buttonText: I18nKey;
|
||||
isComplete?: boolean;
|
||||
isHidden?: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const useGetStartedMetadata = () => {
|
||||
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 isLoadingDemoApp = !demoApp && !error;
|
||||
const hideDemo = error?.status === 404;
|
||||
|
||||
const data: GetStartedMetadata[] = [
|
||||
{
|
||||
|
@ -31,6 +46,7 @@ const useGetStartedMetadata = () => {
|
|||
icon: checkDemoIcon,
|
||||
buttonText: 'general.check_out',
|
||||
isComplete: settings?.checkDemo,
|
||||
isHidden: hideDemo,
|
||||
onClick: async () => {
|
||||
void updateSettings({ checkDemo: true });
|
||||
window.open('/demo-app', '_blank');
|
||||
|
@ -97,6 +113,7 @@ const useGetStartedMetadata = () => {
|
|||
data,
|
||||
completedCount: data.filter(({ isComplete }) => isComplete).length,
|
||||
totalCount: data.length,
|
||||
isLoading: isLoadingDemoApp,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -9,13 +9,14 @@ import ConfirmModal from '@/components/ConfirmModal';
|
|||
import Spacer from '@/components/Spacer';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
|
||||
import Skeleton from './components/Skeleton';
|
||||
import useGetStartedMetadata from './hook';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const GetStarted = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const navigate = useNavigate();
|
||||
const { data } = useGetStartedMetadata();
|
||||
const { data, isLoading } = useGetStartedMetadata();
|
||||
const { update } = useUserPreferences();
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
|
||||
|
@ -45,23 +46,28 @@ const GetStarted = () => {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{data.map(({ id, title, subtitle, icon, isComplete, buttonText, onClick }) => (
|
||||
<Card key={id} className={styles.card}>
|
||||
<img className={styles.icon} src={icon} />
|
||||
{isComplete && <img className={styles.completeIndicator} src={completeIndicator} />}
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.subtitle}>{t(subtitle)}</div>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="outline"
|
||||
size="large"
|
||||
title={buttonText}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
{isLoading && <Skeleton />}
|
||||
{!isLoading &&
|
||||
data.map(
|
||||
({ id, title, subtitle, icon, isComplete, isHidden, buttonText, onClick }) =>
|
||||
!isHidden && (
|
||||
<Card key={id} className={styles.card}>
|
||||
<img className={styles.icon} src={icon} />
|
||||
{isComplete && <img className={styles.completeIndicator} src={completeIndicator} />}
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.subtitle}>{t(subtitle)}</div>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
type="outline"
|
||||
size="large"
|
||||
title={buttonText}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
<ConfirmModal
|
||||
title="get_started.confirm"
|
||||
isOpen={showConfirmModal}
|
||||
|
|
Loading…
Reference in a new issue