import useFetch from 'fetch-suspense'; import Tooltip from 'rc-tooltip'; import React, { Suspense, useEffect, useState } from 'react'; import { OutboundLink } from 'react-ga'; import { useTranslation } from 'react-i18next'; import { GoInfo } from 'react-icons/go'; import { IoIosFlash } from 'react-icons/io'; import BarLoader from 'react-spinners/BarLoader'; import styled from 'styled-components'; import { useStoreActions } from '../../store'; import { sendError, sendExpandEvent } from '../../util/analytics'; import { mobile } from '../../util/css'; export const COLORS = { available: '#6e00ff', unavailable: 'darkgrey', error: '#ff388b', }; export const Card: React.FC<{ title: string }> = ({ title, children }) => { return ( {title} {children} ); }; export interface CommonRepeaterProps { items: string[]; moreItems?: string[]; } export interface MultiShotRepeaterProps extends CommonRepeaterProps { singleShot?: false; children: (name: string) => React.ReactNode; } export interface SingleShotRepeaterProps extends CommonRepeaterProps { singleShot: true; children: (name: string[]) => React.ReactNode; } export type RepeaterProps = MultiShotRepeaterProps | SingleShotRepeaterProps; export const Repeater: React.FC = (props) => { const [revealAlternatives, setRevealAlternatives] = useState(false); const { t } = useTranslation(); function onClick() { sendExpandEvent(); setRevealAlternatives(true); } useEffect(() => { setRevealAlternatives(false); }, [props.items, props.moreItems]); const { items, moreItems = [] } = props; return ( <> {props.singleShot === true ? ( {props.children(items)} ) : ( items.map((name) => ( {props.children(name)} )) )} {revealAlternatives ? ( props.singleShot ? ( {props.children(moreItems)} ) : ( moreItems.map((name) => ( {props.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 increaseCounter = useStoreActions((actions) => actions.stats.add); const response = useFetch( `/api/services/${service}/${encodeURIComponent(query || name)}` ) as Response; if (response.error) { throw new APIError(`${service}: ${response.error}`); } useEffect(() => { increaseCounter(response.availability); // eslint-disable-next-line }, []); 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 increaseCounter = useStoreActions((actions) => actions.stats.add); 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; useEffect(() => { increaseCounter(availability); // eslint-disable-next-line }, []); 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; font-size: 1rem; line-height: 1rem; ${mobile} { margin-bottom: 40px; padding: 0px; } `; const CardTitle = styled.div` margin-bottom: 15px; font-size: 1em; font-weight: bold; ${mobile} { padding: 0 20px; margin-bottom: 20px; font-size: 1.2rem; font-weight: 600; } `; const CardContent = styled.div` border-radius: 2px; ${mobile} { padding: 20px; box-shadow: 0px 2px 20px rgba(0, 0, 0, 0.1); background: white; border-radius: 0; font-size: 1.2em; } `; 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; padding: 4px 0; `; export const ResultIcon = styled.div` width: 1em; `; export const ResultItem = styled.div` display: flex; flex-direction: row; align-items: flex-start; word-break: break-all; color: ${({ color }) => color}; `; export const ResultName = styled.div` margin-left: 6px; font-family: monospace; a { text-decoration: none; color: inherit; } `; export const AvailableIcon = styled.div` margin-top: 2px; margin-left: 3px; padding: 0; width: 15px; text-align: center; font-size: 13px; height: 15px; `;