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 { 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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue