import * as React from 'react' ; import type { HeadersFunction , LinkProps } from 'remix' ; import { Link } from 'remix' ; import type { NonNullProperties , User } from '~/types' ; import { Team } from '@prisma/client' ; import * as dateFns from 'date-fns' ; import md5 from 'md5-hash' ; import { images } from '../images' ; import type { getEnv } from './env.server' ; const teams = Object . values ( Team ) ; const isTeam = ( team ? ) : team is Team => teams . includes ( team as Team ) ; const unknownTeam = { UNKNOWN : 'UNKNOWN' } as const ; const optionalTeams = { ... Team , ... unknownTeam } ; type OptionalTeam = typeof optionalTeams [ keyof typeof optionalTeams ] ; const defaultAvatarSize = 128 ; function getAvatar ( email , { size = defaultAvatarSize , fallback = images . kodyProfileWhite ( { resize : { width : size } } ) , origin , } = { } ) { const hash = md5 ( email ) ; const url = new URL ( `https://www.gravatar.com/avatar/${ hash }` ) ; url . searchParams . set ( 'size' , String ( size ) ) ; if ( fallback ) { if ( origin && fallback . startsWith ( '/' ) ) { fallback = `${ origin }${ fallback }` ; } url . searchParams . set ( 'default' , fallback ) ; } return url . toString ( ) ; } const avatarFallbacks = { BLUE : ( width ) => images . kodyProfileBlue ( { resize : { width } } ) , RED : ( width ) => images . kodyProfileRed ( { resize : { width } } ) , YELLOW : ( width ) => images . kodyProfileYellow ( { resize : { width } } ) , } ; function getAvatarForUser ( { email , team , firstName } , { size = defaultAvatarSize , origin } = { } ) { return { src : getAvatar ( email , { fallback : avatarFallbacks [ team ] ( size ) , size , origin , } ) , alt : firstName , } ; } const teamTextColorClasses = { YELLOW : 'text-team-yellow' , BLUE : 'text-team-blue' , RED : 'text-team-red' , UNKNOWN : 'text-team-unknown' , } ; const teamDisplay = { RED : 'Red' , BLUE : 'Blue' , YELLOW : 'Yellow' , } ; const useSSRLayoutEffect = typeof window === 'undefined' ? ( ) => { } : React . useLayoutEffect ; type AnchorProps = React . DetailedHTMLProps < React . AnchorHTMLAttributes < HTMLAnchorElement > , HTMLAnchorElement > ; const AnchorOrLink = React . forwardRef < HTMLAnchorElement , AnchorProps & { reload ? : boolean ; to ? : LinkProps [ 'to' ] ; prefetch ? : LinkProps [ 'prefetch' ] ; } > ( function AnchorOrLink ( props , ref ) { const { to , href , download , reload = false , prefetch , children , ... rest } = props ; let toUrl = '' ; let shouldUserRegularAnchor = reload || download ; if ( ! shouldUserRegularAnchor && typeof href === 'string' ) { shouldUserRegularAnchor = href . includes ( ':' ) || href . startsWith ( '#' ) ; } if ( ! shouldUserRegularAnchor && typeof to === 'string' ) { toUrl = to ; shouldUserRegularAnchor = to . includes ( ':' ) ; } if ( ! shouldUserRegularAnchor && typeof to === 'object' ) { toUrl = `${ to . pathname ?? '' }${ to . hash ? `#${ to . hash }` : '' }${ to . search ? `?${ to . search }` : '' }` ; shouldUserRegularAnchor = to . pathname ?. includes ( ':' ) ; } if ( shouldUserRegularAnchor ) { return ( < a { ... rest } download = { download } href = { href ?? toUrl } ref = { ref } > { children } < /a>); } else { return ( < Link prefetch = { prefetch } to = { to ?? href ?? '' } { ... rest } ref = { ref } > { children } < /Link>); } } ) ; function formatTime ( seconds ) { return dateFns . format ( dateFns . addSeconds ( new Date ( 0 ) , seconds ) , 'mm:ss' ) ; } const formatNumber = ( num ) => new Intl . NumberFormat ( ) . format ( num ) ; function formatAbbreviatedNumber ( num ) { return num < 1000 ? formatNumber ( num ) : num < 1000000 ? `${ formatNumber ( Number ( ( num / 1000 ) . toFixed ( 2 ) ) ) }k` : num < 1000000000 ? `${ formatNumber ( Number ( ( num / 1000000 ) . toFixed ( 2 ) ) ) }m` : num < 1000000000000 ? `${ formatNumber ( Number ( ( num / 1000000000 ) . toFixed ( 2 ) ) ) }b` : 'a lot' ; } function formatDate ( dateString ) { return dateFns . format ( dateFns . add ( dateFns . parseISO ( dateString ) , { minutes : new Date ( ) . getTimezoneOffset ( ) , } ) , 'PPP' ) ; } function getErrorMessage ( error ) { if ( typeof error === 'string' ) return error ; if ( error instanceof Error ) return error . message ; return 'Unknown Error' ; } function getErrorStack ( error ) { if ( typeof error === 'string' ) return error ; if ( error instanceof Error ) return error . stack ; return 'Unknown Error' ; } function getNonNull < Type extends Record < string , null | undefined | unknown > > ( obj ) { for ( const [ key , val ] of Object . entries ( obj ) ) { assertNonNull ( val , `The value of ${ key } is null but it should not be.` ) ; } return obj as NonNullProperties < Type > ; } function typedBoolean < T > ( value ) { return Boolean ( value ) ; } function assertNonNull < PossibleNullType > ( possibleNull , errorMessage ) { if ( possibleNull == null ) throw new Error ( errorMessage ) ; } function getRequiredEnvVarFromObj ( obj , key , devValue = `${ key }-dev-value` ) { let value = devValue ; const envVal = obj [ key ] ; if ( envVal ) { value = envVal ; } else if ( obj . NODE_ENV === 'production' ) { throw new Error ( `${ key } is a required env variable` ) ; } return value ; } function getRequiredServerEnvVar ( key , devValue ? ) { return getRequiredEnvVarFromObj ( process . env , key , devValue ) ; } function getRequiredGlobalEnvVar ( key , devValue ? ) { return getRequiredEnvVarFromObj ( ENV , key , devValue ) ; } function getDiscordAuthorizeURL ( domainUrl ) { const url = new URL ( 'https://discord.com/api/oauth2/authorize' ) ; url . searchParams . set ( 'client_id' , getRequiredGlobalEnvVar ( 'DISCORD_CLIENT_ID' ) ) ; url . searchParams . set ( 'redirect_uri' , `${ domainUrl }/discord/callback` ) ; url . searchParams . set ( 'response_type' , 'code' ) ; url . searchParams . set ( 'scope' , 'identify guilds.join email guilds' ) ; return url . toString ( ) ; } function getDomainUrl ( request ) { const host = request . headers . get ( 'X-Forwarded-Host' ) ?? request . headers . get ( 'host' ) ; if ( ! host ) { throw new Error ( 'Could not determine domain URL.' ) ; } const protocol = host . includes ( 'localhost' ) ? 'http' : 'https' ; return `${ protocol }://${ host }` ; } function removeTrailingSlash ( s ) { return s . endsWith ( '/' ) ? s . slice ( 0 , - 1 ) : s ; } function getDisplayUrl ( requestInfo ? ) { return getUrl ( requestInfo ) . replace ( /^https?:\/\// , '' ) ; } function getUrl ( requestInfo ? ) { return removeTrailingSlash ( `${ requestInfo ?. origin ?? 'https://kentcdodds.com' }${ requestInfo ?. path ?? '' }` ) ; } function toBase64 ( string ) { if ( typeof window === 'undefined' ) { return Buffer . from ( string ) . toString ( 'base64' ) ; } else { return window . btoa ( string ) ; } } function useUpdateQueryStringValueWithoutNavigation ( queryKey , queryValue ) { React . useEffect ( ( ) => { const currentSearchParams = new URLSearchParams ( window . location . search ) ; const oldQuery = currentSearchParams . get ( queryKey ) ?? '' ; if ( queryValue === oldQuery ) return ; if ( queryValue ) { currentSearchParams . set ( queryKey , queryValue ) ; } else { currentSearchParams . delete ( queryKey ) ; } const newUrl = [ window . location . pathname , currentSearchParams . toString ( ) ] . filter ( Boolean ) . join ( '?' ) ; window . history . replaceState ( null , '' , newUrl ) ; } , [ queryKey , queryValue ] ) ; } function debounce < Callback extends ( ... args : Array < unknown > ) => void > ( fn , delay ) { let timer = null ; return ( ... args ) => { if ( timer ) clearTimeout ( timer ) ; timer = setTimeout ( ( ) => { fn ( ... args ) ; } , delay ) ; } ; } function useDebounce < Callback extends ( ... args : Array < unknown > ) => unknown > ( callback , delay ) { const callbackRef = React . useRef ( callback ) ; React . useEffect ( ( ) => { callbackRef . current = callback ; } ) ; return React . useMemo ( ( ) => debounce ( ( ... args ) => callbackRef . current ( ... args ) , delay ) , [ delay ] ) ; } const reuseUsefulLoaderHeaders = ( { loaderHeaders } ) => { const headers = new Headers ( ) ; const usefulHeaders = [ 'Cache-Control' , 'Vary' , 'Server-Timing' ] ; for ( const headerName of usefulHeaders ) { if ( loaderHeaders . has ( headerName ) ) { headers . set ( headerName , loaderHeaders . get ( headerName ) ! ) ; } } return headers ; } ; function callAll < Args extends Array < unknown > > ( ... fns ) { return ( ... args ) => fns . forEach ( fn => fn ?. ( ... args ) ) ; } function useDoubleCheck ( ) { const [ doubleCheck , setDoubleCheck ] = React . useState ( false ) ; function getButtonProps ( props ? ) { const onBlur = ( ) => setDoubleCheck ( false ) ; const onClick = doubleCheck ? undefined : e => { e . preventDefault ( ) ; setDoubleCheck ( true ) ; } ; return { ... props , onBlur : callAll ( onBlur , props ?. onBlur ) , onClick : callAll ( onClick , props ?. onClick ) , } ; } return { doubleCheck , getButtonProps } ; } export { getAvatar , getAvatarForUser , AnchorOrLink , getErrorMessage , getErrorStack , getNonNull , assertNonNull , useUpdateQueryStringValueWithoutNavigation , useSSRLayoutEffect , useDoubleCheck , useDebounce , typedBoolean , getRequiredServerEnvVar , getRequiredGlobalEnvVar , getDiscordAuthorizeURL , getDomainUrl , getUrl , getDisplayUrl , toBase64 , removeTrailingSlash , reuseUsefulLoaderHeaders , unknownTeam , teams , isTeam , teamDisplay , teamTextColorClasses , formatDate , formatTime , formatNumber , formatAbbreviatedNumber , } ; export { listify } from './listify' ; export type { OptionalTeam } ;