import fetch from 'isomorphic-unfetch' ; import LRU from 'lru-cache' ; import { NextPageContext } from 'next' ; import getConfig from 'next/config' ; import React , { createContext , ReactElement , useContext } from 'react' ; import { MembershipRank } from './types' ; import { ALL_CLUB_FIELDS , CLUB_FIELDS , MEMBERSHIP_ROLE_NAMES , } from './utils/branding' ; const dev = process . env . NODE_ENV !== 'production' ; /**
 * Returns true if this is a development environment, or false otherwise.
 */ export function isDevelopment ( ) { return dev ; } const internalCache = new LRU ( ) ; /**
 * Cache the return value of a function.
 * This should only be used with publically available information.
 * The time should be specified in milliseconds.
 */ export async function cache < T > ( key , func , time ) { const cached = internalCache . get ( key ) ; if ( cached != null ) { return cached ; } const val = await func ( ) ; internalCache . set ( key , val , time ) ; return val ; } export function stripTags ( val ) { if ( ! val ) { return val ; } return val . replace ( /(<[^>]+>)/gi , '' ) . replace ( '&amp;' , '&' ) . replace ( '&lt;' , '<' ) . replace ( '&gt;' , '>' ) . replace ( '&ndash;' , '-' ) . replace ( '&mdash;' , '-' ) . replace ( '&nbsp;' , ' ' ) . trim ( ) ; } export const OptionsContext = createContext ( { } ) ; param key export function useSetting ( key ) { const options = useContext ( OptionsContext ) ; const value = options [ key ] ?? null ; if ( value === 'true' ) { return true ; } if ( value === 'false' ) { return false ; } return value ; } export function getCurrentRelativePath ( ) { return ( window . location . pathname + window . location . search + window . location . hash ) ; } export function getDefaultClubImageURL ( ) { return '/static/img/hatlogo.png' ; } const { publicRuntimeConfig } = getConfig ( ) ; export const SITE_ORIGIN = publicRuntimeConfig . SITE_ORIGIN ; export const API_BASE_URL = `${ SITE_ORIGIN }/api` ; export const EMPTY_DESCRIPTION = '<span style="color:#666">This club has not added a description yet.</span>' ; export const LOGIN_URL = `${ API_BASE_URL }/accounts/login/` ; export const LOGOUT_URL = `${ API_BASE_URL }/accounts/logout/` ; export function getSizeDisplay ( size , showMembersLabel = true ) { const postfix = showMembersLabel ? ' Members' : '' ; if ( size === 1 ) return '< 20' + postfix ; else if ( size === 2 ) return '20 - 50' + postfix ; else if ( size === 3 ) return '50 - 100' + postfix ; else if ( size === 4 ) return '> 100' + postfix ; else return 'Unknown' ; } export function getRoleDisplay ( role ) { return MEMBERSHIP_ROLE_NAMES [ role ] ?? 'Unknown' ; } export function getApiUrl ( path ) { if ( /^https?:\/\// . test ( path ) ) { const url = new URL ( path ) ; return url . pathname + url . search ; } return API_BASE_URL + path ; } export const PermissionsContext = React . createContext < { [ key ] : boolean | null ; } > ( { } ) ; /**
 * Prefetch some permissions for the current page.
 * Should be executed at the start of each page with permissions.
 */ export async function preloadPermissions ( permissions ) { const resp = await doApiRequest ( `/settings/permissions/?perm=${ encodeURIComponent ( permissions . join ( ',' ) ) }&format=json` ) ; if ( ! resp . ok ) { return { } ; } return ( await resp . json ( ) ) . permissions ; } /**
 * Check if the user has a Django permission.
 * If you were checking for the "clubs.approve_club" permission, pass the
 * string "clubs.approve_club".
 *
 * If you are checking for a permission that is object based, separate the
 * object identiifer with a colon. For example, "permission:object_id".
 */ export function apiCheckPermission ( permission , failSilently ? ) { const perms = useContext ( PermissionsContext ) ; if ( Array . isArray ( permission ) ) { return permission . some ( ( perm ) => perm in perms && perms [ perm ] ) ; } if ( permission in perms ) { return perms [ permission ] ; } if ( failSilently ) { return false ; } const msg = `The permission '${ permission }' was not preloaded on this page. You should preload this permission for efficiency purposes. Loaded permissions: ${ JSON . stringify ( perms ) }` ; if ( isDevelopment ( ) ) { throw new Error ( msg ) ; } return false ; } const chooseEndpoint = ( input ) => { if ( typeof input === 'string' ) { return `/${ input }/?format=json` ; } else if ( input [ 1 ] . startsWith ( '/' ) ) { return input [ 1 ] ; } return chooseEndpoint ( input [ 1 ] ) ; } ; /**
 * Lookup a lot of endpoints asynchronously.
 * Should be used primarily by getInitialProps methods.
 */ export async function doBulkLookup ( paths , ctx ? ) { const data = ctx != null ? { headers : ctx . req ? { cookie : ctx . req . headers . cookie } : undefined , } : undefined ; const resps = await Promise . all ( paths . map ( ( item ) => doApiRequest ( chooseEndpoint ( item ) , data ) . then ( async ( resp ) => { const contents = await resp . text ( ) ; try { return JSON . parse ( contents ) ; } catch ( e ) { console . error ( `Body contents of failed JSON parsing: ${ contents }` ) ; throw e ; } } ) ) ) ; const output = { } ; for ( let i = 0 ; i < paths . length ; i ++ ) { output [ typeof paths [ i ] === 'string' ? ( paths [ i ] as string ) : paths [ i ] [ 0 ] ] = resps [ i ] ; } return output ; } param path param data export function doApiRequest ( path , data ? ) { if ( ! data ) { data = { } ; } data . credentials = 'include' ; if ( typeof document !== 'undefined' ) { data . headers = data . headers || { } ; if ( ! ( data . body instanceof FormData ) ) { data . headers [ 'Content-Type' ] = 'application/json' ; } data . headers [ 'X-CSRFToken' ] = ( /csrftoken=(\w+)/ . exec ( document . cookie ) || [ null , null , ] ) [ 1 ] ; } if ( data . body && ! ( data . body instanceof FormData ) ) { data . body = JSON . stringify ( data . body ) ; } return fetch ( getApiUrl ( path ) , data as RequestInit ) ; } export function apiSetFavoriteStatus ( club , favorited ) { if ( favorited ) { return doApiRequest ( '/favorites/?format=json' , { method : 'POST' , body : { club : club } , } ) . then ( ( ) => undefined ) ; } else { return doApiRequest ( `/favorites/${ club }/?format=json` , { method : 'DELETE' , } ) . then ( ( ) => undefined ) ; } } export function apiSetSubscribeStatus ( club , subscribed ) { if ( subscribed ) { return doApiRequest ( '/subscriptions/?format=json' , { method : 'POST' , body : { club : club , } , } ) . then ( ( ) => undefined ) ; } else { return doApiRequest ( `/subscriptions/${ club }/?format=json` , { method : 'DELETE' , } ) . then ( ( ) => undefined ) ; } } param code param id param liked export function apiSetLikeStatus ( code , id , liked ) { const path = liked ? 'like' : 'unlike' ; return doApiRequest ( `/clubs/${ code }/questions/${ id }/${ path }/?format=json` , { method : 'POST' , } ) ; } /**
 * Convert underscores into spaces and capitalize the first letter of every word.
 */ export function titleize ( str ) { if ( ! str ) return str ; return str . replace ( /_+/g , ' ' ) . split ( ' ' ) . map ( ( a ) => a [ 0 ] . toUpperCase ( ) + a . substr ( 1 ) . toLowerCase ( ) ) . join ( ' ' ) ; } /**
 * Given a dictionary that you want to display to the user, converts this
 * dictionary into a displayable list of key value pairs.
 */ export function formatResponse ( err ) { if ( typeof err === 'string' ) { return < > { err } < />; } return ( < > { Object . keys ( err ) . filter ( ( line ) => ! ( line === 'success' && typeof err [ line ] === 'boolean' ) ) . filter ( ( line ) => ! ( typeof err [ line ] === 'number' ) ) . map ( ( line ) => ( < div key = { line } > < b > { titleize ( line ) } : < /b> {err[line]} < /div>))} < />); } /**
 * Indicates whether a field is shown on the edit page
 * and the club information page.
 *
 * Looks up information from current branding to see which
 * fields should be displayed.
 */ export function isClubFieldShown ( name ) { if ( ALL_CLUB_FIELDS . has ( name ) && ! CLUB_FIELDS . has ( name ) ) { return false ; } return true ; } /**
 * Attempt to format a phone number in US phone number format.
 */ export function formatPhoneNumber ( phone ) { const cleaned = ( '' + phone ) . replace ( /\D/g , '' ) ; const match = cleaned . match ( /^(1|)?(\d{3})(\d{3})(\d{4})$/ ) ; if ( match ) { const intlCode = match [ 1 ] ? '+1 ' : '' ; return [ intlCode , '(' , match [ 2 ] , ') ' , match [ 3 ] , '-' , match [ 4 ] ] . join ( '' ) ; } return phone ; } param arr param sep export function intersperse < T , U > ( arr , sep ) { if ( arr . length === 0 ) { return [ ] ; } return arr . slice ( 1 ) . reduce ( ( xs , x , idx ) => { return xs . concat ( [ typeof sep !== 'string' ? sep ( idx ) : sep , x ] ) ; } , [ arr [ 0 ] ] ) ; } /**
 * Return the starting year of the current school year that we are in.
 * For example, if we are in Fall 2019 or Spring 2020, the current school year should be 2019.
 */ export function getCurrentSchoolYear ( ) { const now = new Date ( ) ; let year = now . getFullYear ( ) ; if ( now . getMonth ( ) < 6 ) { year -= 1 ; } return year ; } /**
 * Given a date, return a semester string corresponding to that date.
 * For example, if 1/8/2021 was passed in, the return result should be "Spring 2021".
 * Does not support the summer semester.
 */ export function getSemesterFromDate ( date ) { if ( typeof date === 'string' ) { date = new Date ( date ) ; } const year = date . getFullYear ( ) ; const sem = date . getMonth ( ) >= 6 ? 'Fall' : 'Spring' ; return `${ sem } ${ year }` ; } export const bifurcateFilter = ( arr , filter ) => arr . reduce ( ( [ trueArray , falseArray ] , cur ) => filter ( cur ) ? [ [ ... trueArray , cur ] , falseArray ] : [ trueArray , [ ... falseArray , cur ] ] , [ [ ] , [ ] ] ) ; export const categorizeFilter = ( arr , filter ) => arr . reduce ( ( acc , cur ) => { const key = filter ( cur ) ; return { ... acc , [ key ] : acc [ key ] ? [ ... acc [ key ] , cur ] : [ cur ] , } ; } , { } ) ;