Refactor BundleColumnError to TS (#39177)

This commit is contained in:
diondiondion 2026-05-27 12:33:52 +02:00 committed by GitHub
parent bd2e86d7f4
commit b5879fd61f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 173 additions and 181 deletions

View File

@ -13,8 +13,7 @@
.content {
max-width: 370px;
svg,
img {
:where(svg, img) {
width: 200px;
aspect-ratio: 1;
object-fit: contain;
@ -22,16 +21,10 @@
margin-bottom: 16px;
}
h3 {
font-size: 17px;
font-weight: 500;
text-wrap: balance;
line-height: 1.2;
}
p {
margin-top: 8px;
font-size: 15px;
line-height: 1.4;
color: var(--color-text-secondary);
text-wrap: pretty;
}
@ -41,6 +34,18 @@
}
}
.heading {
font-size: 17px;
font-weight: 500;
text-wrap: balance;
line-height: 1.2;
}
.errorImage {
width: 280px;
margin: -10% 0;
}
[data-color-scheme='dark'] .defaultImage {
--color-skin-1: #3a3a50;
--color-skin-2: #67678e;

View File

@ -29,6 +29,14 @@ export const Default: Story = {
},
};
export const Error: Story = {
args: {
image: 'error',
title: 'Error',
message: 'Something went wrong loading the page.',
},
};
export const WithAction: Story = {
args: {
...Default.args,

View File

@ -4,10 +4,15 @@ import classNames from 'classnames';
import ElephantImage from '@/images/elephant_ui.svg?react';
import { GIF } from '../gif';
import classes from './empty_state.module.scss';
const images = {
default: <ElephantImage className={classes.defaultImage} />,
error: (
<GIF src='/oops.gif' staticSrc='/oops.png' className={classes.errorImage} />
),
};
/**
@ -21,6 +26,7 @@ export const EmptyState: React.FC<{
title?: React.ReactNode;
message?: React.ReactNode;
children?: React.ReactNode;
headingLevel?: 'h2' | 'h3' | 'h4';
className?: string;
}> = ({
image = 'default',
@ -29,6 +35,7 @@ export const EmptyState: React.FC<{
),
message,
children,
headingLevel: Heading = 'h2',
className,
}) => {
const imageToRender = typeof image === 'string' ? images[image] : image;
@ -38,7 +45,7 @@ export const EmptyState: React.FC<{
{(title || message || imageToRender) && (
<div className={classes.content}>
{imageToRender}
{!!title && <h3>{title}</h3>}
{!!title && <Heading className={classes.heading}>{title}</Heading>}
{!!message && <p>{message}</p>}
</div>
)}

View File

@ -4,7 +4,7 @@ import { autoPlayGif } from 'mastodon/initial_state';
export const GIF: React.FC<{
src: string;
staticSrc: string;
className: string;
className?: string;
animate?: boolean;
}> = ({ src, staticSrc, className, animate = autoPlayGif }) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);

View File

@ -9,7 +9,7 @@ import { Helmet } from '@unhead/react/helmet';
import { Column } from '@/mastodon/components/column';
import { ColumnHeader } from '@/mastodon/components/column_header';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error';
import { useColumnsContext } from '../../ui/util/columns_context';
import classes from '../styles.module.scss';

View File

@ -19,7 +19,7 @@ import {
} from '@/mastodon/components/scrollable_list/components';
import type { TruncatedListItemInfo } from '@/mastodon/components/truncated_list';
import { TruncatedListItems } from '@/mastodon/components/truncated_list';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error';
import Column from '@/mastodon/features/ui/components/column';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId';

View File

@ -11,7 +11,7 @@ import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
import { RemoteHint } from '@/mastodon/components/remote_hint';
import ScrollableList from '@/mastodon/components/scrollable_list';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error';
import Column from '@/mastodon/features/ui/components/column';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';

View File

@ -19,7 +19,7 @@ import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { RemoteHint } from '@/mastodon/components/remote_hint';
import StatusList from '@/mastodon/components/status_list';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error';
import {
useAccountId,
useCurrentAccountId,

View File

@ -6,7 +6,7 @@ import { Column } from '@/mastodon/components/column';
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import ScrollableList from '@/mastodon/components/scrollable_list';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import { useLayout } from '@/mastodon/hooks/useLayout';

View File

@ -14,7 +14,7 @@ import { Helmet } from '@unhead/react/helmet';
import { apiGetTermsOfService } from 'mastodon/api/instance';
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
import { Column } from 'mastodon/components/column';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { BundleColumnError } from 'mastodon/features/ui/components/bundle_column_error';
const messages = defineMessages({
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },

View File

@ -1,116 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from '@unhead/react/helmet';
import { Link } from 'react-router-dom';
import { Button } from 'mastodon/components/button';
import Column from 'mastodon/components/column';
import { injectIntl } from '@/mastodon/components/intl';
import { GIF } from 'mastodon/components/gif';
class CopyButton extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
value: PropTypes.string.isRequired,
};
state = {
copied: false,
};
handleClick = () => {
const { value } = this.props;
navigator.clipboard.writeText(value);
this.setState({ copied: true });
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
};
componentWillUnmount () {
if (this.timeout) clearTimeout(this.timeout);
}
render () {
const { children } = this.props;
const { copied } = this.state;
return (
<Button onClick={this.handleClick} className={copied ? 'copied' : 'copyable'}>{copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : children}</Button>
);
}
}
class BundleColumnError extends PureComponent {
static propTypes = {
errorType: PropTypes.oneOf(['routing', 'network', 'error']),
onRetry: PropTypes.func,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
stacktrace: PropTypes.string,
};
static defaultProps = {
errorType: 'routing',
};
handleRetry = () => {
const { onRetry } = this.props;
if (onRetry) {
onRetry();
}
};
render () {
const { errorType, multiColumn, stacktrace } = this.props;
let title, body;
switch(errorType) {
case 'routing':
title = <FormattedMessage id='bundle_column_error.routing.title' defaultMessage='404' />;
body = <FormattedMessage id='bundle_column_error.routing.body' defaultMessage='The requested page could not be found. Are you sure the URL in the address bar is correct?' />;
break;
case 'network':
title = <FormattedMessage id='bundle_column_error.network.title' defaultMessage='Network error' />;
body = <FormattedMessage id='bundle_column_error.network.body' defaultMessage='There was an error when trying to load this page. This could be due to a temporary problem with your internet connection or this server.' />;
break;
case 'error':
title = <FormattedMessage id='bundle_column_error.error.title' defaultMessage='Oh, no!' />;
body = <FormattedMessage id='bundle_column_error.error.body' defaultMessage='The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.' />;
break;
}
return (
<Column bindToDocument={!multiColumn}>
<div className='error-column'>
<GIF src='/oops.gif' staticSrc='/oops.png' className='error-column__image' />
<div className='error-column__message'>
<h1>{title}</h1>
<p>{body}</p>
<div className='error-column__message__actions'>
{errorType === 'network' && <Button onClick={this.handleRetry}><FormattedMessage id='bundle_column_error.retry' defaultMessage='Try again' /></Button>}
{errorType === 'error' && <CopyButton value={stacktrace}><FormattedMessage id='bundle_column_error.copy_stacktrace' defaultMessage='Copy error report' /></CopyButton>}
<Link to='/' className={classNames('button', { 'button-secondary': errorType !== 'routing' })}><FormattedMessage id='bundle_column_error.return' defaultMessage='Go back home' /></Link>
</div>
</div>
</div>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default injectIntl(BundleColumnError);

View File

@ -0,0 +1,122 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Helmet } from '@unhead/react/helmet';
import { CopyButton } from '@/mastodon/components/copy_button';
import { EmptyState } from '@/mastodon/components/empty_state';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
import classes from './styles.module.scss';
interface BundleColumnErrorProps {
errorType?: 'routing' | 'network' | 'error';
onRetry?: () => void;
multiColumn?: boolean;
stacktrace?: string;
}
export const BundleColumnError: React.FC<BundleColumnErrorProps> = ({
errorType = 'routing',
onRetry,
multiColumn,
stacktrace,
}) => {
let title, body;
switch (errorType) {
case 'routing':
title = (
<FormattedMessage
id='bundle_column_error.routing.title'
defaultMessage='404'
/>
);
body = (
<FormattedMessage
id='bundle_column_error.routing.body'
defaultMessage='The requested page could not be found. Are you sure the URL in the address bar is correct?'
/>
);
break;
case 'network':
title = (
<FormattedMessage
id='bundle_column_error.network.title'
defaultMessage='Network error'
/>
);
body = (
<FormattedMessage
id='bundle_column_error.network.body'
defaultMessage='There was an error when trying to load this page. This could be due to a temporary problem with your internet connection or this server.'
/>
);
break;
case 'error':
title = (
<FormattedMessage
id='bundle_column_error.error.title'
defaultMessage='Oh, no!'
/>
);
body = (
<FormattedMessage
id='bundle_column_error.error.body'
defaultMessage='The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.'
/>
);
break;
}
return (
<Column bindToDocument={!multiColumn}>
<EmptyState
image='error'
title={title}
message={body}
className={classes.error}
>
<div className={classes.actions}>
{errorType === 'network' && onRetry && (
<Button onClick={onRetry}>
<FormattedMessage
id='bundle_column_error.retry'
defaultMessage='Try again'
/>
</Button>
)}
{errorType === 'error' && stacktrace && (
<CopyButton value={stacktrace}>
<FormattedMessage
id='bundle_column_error.copy_stacktrace'
defaultMessage='Copy error report'
/>
</CopyButton>
)}
<Link
to='/'
className={classNames('button', {
'button-secondary': errorType !== 'routing',
})}
>
<FormattedMessage
id='bundle_column_error.return'
defaultMessage='Go back home'
/>
</Link>
</div>
</EmptyState>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default BundleColumnError;

View File

@ -0,0 +1,12 @@
.error {
flex-grow: 1;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
justify-content: center;
}

View File

@ -33,7 +33,7 @@ import {
import { useColumnsContext } from '../util/columns_context';
import Bundle from './bundle';
import BundleColumnError from './bundle_column_error';
import { BundleColumnError } from './bundle_column_error';
import { ColumnLoading } from './column_loading';
import { ComposePanel, RedirectToMobileComposeIfNeeded } from './compose_panel';
import DrawerLoading from './drawer_loading';

View File

@ -3374,8 +3374,7 @@ a.account__display-name {
.column-header,
.column-back-button,
.scrollable,
.error-column {
.scrollable {
border-radius: 0 !important;
}
@ -5288,51 +5287,6 @@ a.status-card {
margin-bottom: 0;
}
.error-column {
padding: 20px;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
flex-direction: column;
cursor: default;
&__image {
width: 70%;
max-width: 350px;
margin-top: -50px;
}
&__message {
text-align: center;
color: var(--color-text-secondary);
font-size: 15px;
line-height: 22px;
h1 {
font-size: 28px;
line-height: 33px;
font-weight: 700;
margin-bottom: 15px;
color: var(--color-text-primary);
}
p {
max-width: 48ch;
}
&__actions {
margin-top: 30px;
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
}
}
}
@keyframes heartbeat {
0% {
transform: scale(1);