import React, {useState, useEffect, Suspense} from 'react'; import styled from 'styled-components'; import useFetch from 'fetch-suspense'; import {Tooltip} from 'react-tippy'; import 'react-tippy/dist/tippy.css'; import BarLoader from 'react-spinners/BarLoader'; import {GoInfo} from 'react-icons/go'; import {IoIosFlash} from 'react-icons/io'; import {useTranslation} from 'react-i18next'; import {sendError} from '../../util/analytics'; import {mobile} from '../../util/css'; import {ExternalLink} from '../Links'; const COLORS = { available: '#6e00ff', unavailable: 'darkgrey', error: '#ff388b', }; export const Card: React.FC<{title: string}> = ({title, children}) => { return ( {title} {children} ); }; export const Repeater: React.FC<{ items: string[]; moreItems?: string[]; children: (name: string) => React.ReactNode; }> = ({items = [], moreItems = [], children}) => { const [revealAlternatives, setRevealAlternatives] = useState(false); const {t} = useTranslation(); function onClick() { setRevealAlternatives(true); } useEffect(() => { setRevealAlternatives(false); }, [items, moreItems]); return ( <> {items.map((name) => ( {children(name)} ))} {revealAlternatives ? moreItems.map((name) => ( {children(name)} )) : null} {moreItems.length > 0 && !revealAlternatives ? ( ) : null} ); }; interface Response { error?: string; availability: boolean; } class APIError extends Error { constructor(message?: string) { super(message); Object.setPrototypeOf(this, APIError.prototype); } } class NotFoundError extends Error { constructor(message?: string) { super(message); Object.setPrototypeOf(this, NotFoundError.prototype); } } export const DedicatedAvailability: React.FC<{ name: string; query?: string; message?: string; messageIfTaken?: string; service: string; link: string; linkIfTaken?: string; prefix?: string; suffix?: string; icon: React.ReactNode; }> = ({ name, query = undefined, message = '', messageIfTaken = undefined, service, link, linkIfTaken = undefined, prefix = '', suffix = '', icon, }) => { const response = useFetch( `/availability/${service}/${encodeURIComponent(query || name)}`, ) as Response; if (response.error) { throw new APIError(`${service}: ${response.error}`); } return ( ); }; export const ExistentialAvailability: React.FC<{ name: string; target: string; message?: string; messageIfTaken?: string; link: string; linkIfTaken?: string; prefix?: string; suffix?: string; icon: React.ReactNode; }> = ({ name, message = '', messageIfTaken = undefined, target, link, linkIfTaken = undefined, prefix = '', suffix = '', icon, }) => { const response = useFetch(target, undefined, {metadata: true}); if (response.status !== 404 && response.status !== 200) { throw new NotFoundError(`${name}: ${response.status}`); } const availability = response.status === 404; return ( ); }; export const Result: React.FC<{ title: string; message?: string; link?: string; icon: React.ReactNode; prefix?: string; suffix?: string; availability?: boolean; }> = ({ title, message = '', link, icon, prefix = '', suffix = '', availability, }) => { const content = ( <> {prefix} {title} {suffix} ); const itemColor = availability === undefined ? 'inherit' : availability ? COLORS.available : COLORS.unavailable; return ( {icon} {link ? ( {content} ) : ( content )} {availability === true ? : null} ); }; // 1. getDerivedStateFromError // 2. render() // 3. componentDidCatch() send errorInfo to Sentry // 4. render(), now with eventId provided from Sentry class ErrorBoundary extends React.Component< {}, {hasError: boolean; message: string; eventId?: string} > { constructor(props: {}) { super(props); this.state = {hasError: false, message: '', eventId: undefined}; } // used in SSR static getDerivedStateFromError(error: Error) { return {hasError: true, message: error.message}; } // eslint-disable-next-line @typescript-eslint/no-explicit-any componentDidCatch(error: Error, errorInfo: any) { if (error instanceof APIError || error instanceof NotFoundError) { return; } sendError(error, errorInfo).then((eventId) => { this.setState({eventId}); }); } render() { if (this.state.hasError) { return ( Error ); } return this.props.children; } } const ErrorHandler: React.FC = ({children}) => ( }> {children} ); const CardContainer = styled.div` padding: 40px; ${mobile} { margin-bottom: 40px; padding: 0px; } `; const CardTitle = styled.div` margin-bottom: 15px; font-size: 1rem; font-weight: bold; ${mobile} { padding-left: 20px; } `; const CardContent = styled.div` border-radius: 2px; ${mobile} { padding: 20px; box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1); background: white; border-radius: 0; } `; const Button = styled.div` margin-top: 5px; display: inline-block; padding: 5px 0; border: none; border-bottom: 1px dashed black; cursor: pointer; font-family: monospace; font-size: 0.8em; `; const ResultContainer = styled.div` display: flex; align-items: center; margin-top: 8px; `; const ResultIcon = styled.div` width: 1em; `; const ResultItem = styled.div` display: flex; flex-direction: row; align-items: flex-start; word-break: break-all; color: ${({color}) => color}; `; const ResultName = styled.div` margin-left: 6px; line-height: 1em; font-family: monospace; font-size: 1rem; a { text-decoration: none; color: inherit; } `;