()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
- return !emailRegex.test(value) ? 'invalid email' : undefined
+ return !emailRegex.test(value) ? 'Invalid email' : undefined
}
export function password(value: string | undefined): string | undefined {
@@ -48,7 +48,7 @@ export function password(value: string | undefined): string | undefined {
// - at least 1 symbol or number
const passwordRegex: RegExp = /^(?=.*[a-zA-Z])(?=.*[#$^+=!*()@%&\d]).{8,}$/g
- return !passwordRegex.test(value) ? 'password must contain >= 8 characters, >= 1 letter, and >= 1 number or symbol' : undefined
+ return !passwordRegex.test(value) ? 'Password rules: 8+ characters, 1+ letter, and 1+ number or symbol' : undefined
}
export function matchOther(value: string | undefined, formElements?: HTMLFormControlsCollection, otherFieldName?: string): string | undefined {
@@ -67,11 +67,11 @@ export function matchOther(value: string | undefined, formElements?: HTMLFormCon
return undefined
}
- return `does not match the ${getOtherFieldLabel(otherField, otherFieldName)} value`
+ return `Does not match the ${getOtherFieldLabel(otherField, otherFieldName)}`
}
export function required(value: string | undefined): string | undefined {
- return !value ? 'required' : undefined
+ return !value ? 'Required' : undefined
}
export function requiredIfOther(value: string | undefined, formElements?: HTMLFormControlsCollection, otherFieldName?: string): string | undefined {
@@ -87,7 +87,7 @@ export function requiredIfOther(value: string | undefined, formElements?: HTMLFo
return undefined
}
- return `required when ${getOtherFieldLabel(otherField, otherFieldName)} is not blank`
+ return `Required`
}
export function sslUrl(value: string | undefined): string | undefined {
@@ -103,18 +103,14 @@ export function sslUrl(value: string | undefined): string | undefined {
return new URL(value).protocol !== 'https:' ? 'links must start with https' : undefined
} catch {
- return 'invalid url'
+ return 'Invalid URL'
}
}
-export type ValidatorFn = Array<
- (
- value: string | undefined,
- formValues?: HTMLFormControlsCollection,
- otherField?: string,
- )
- => string | undefined
->
+export interface ValidatorFn {
+ dependentField?: string,
+ validator: (value: string | undefined, formValues?: HTMLFormControlsCollection, otherField?: string) => string | undefined
+}
function getOtherField(formElements?: HTMLFormControlsCollection, otherFieldName?: string): HTMLInputElement {
@@ -135,5 +131,5 @@ function getOtherField(formElements?: HTMLFormControlsCollection, otherFieldName
}
function getOtherFieldLabel(otherField: HTMLInputElement, otherFieldName?: string): string {
- return otherField.labels?.[0].firstChild?.nodeValue || otherFieldName as string
+ return (otherField.labels?.[0].firstChild as any)?.innerText || otherFieldName as string
}
diff --git a/src/lib/functions/analytics-functions/analytics.functions.ts b/src-ts/lib/functions/analytics-functions/analytics.functions.ts
similarity index 100%
rename from src/lib/functions/analytics-functions/analytics.functions.ts
rename to src-ts/lib/functions/analytics-functions/analytics.functions.ts
diff --git a/src/lib/functions/analytics-functions/index.ts b/src-ts/lib/functions/analytics-functions/index.ts
similarity index 100%
rename from src/lib/functions/analytics-functions/index.ts
rename to src-ts/lib/functions/analytics-functions/index.ts
diff --git a/src/lib/functions/authentication-functions/authentication-url.config.ts b/src-ts/lib/functions/authentication-functions/authentication-url.config.ts
similarity index 69%
rename from src/lib/functions/authentication-functions/authentication-url.config.ts
rename to src-ts/lib/functions/authentication-functions/authentication-url.config.ts
index 39d8ef9e2..af1c2293d 100644
--- a/src/lib/functions/authentication-functions/authentication-url.config.ts
+++ b/src-ts/lib/functions/authentication-functions/authentication-url.config.ts
@@ -6,7 +6,9 @@ export function login(fallback: string): string {
return `${authentication}?retUrl=${encodeURIComponent(window.location.href.match(/[^?]*/)?.[0] || fallback)}`
}
-export const logout: string = `${authentication}?logout=true&retUrl=${encodeURIComponent('https://' + window.location.host)}`
+export function logout(loggedOutRoute: string): string {
+ return `${authentication}?logout=true&retUrl=${encodeURIComponent('https://' + window.location.host)}${loggedOutRoute}`
+}
export function signup(fallback: string): string {
return `${login(fallback)}®Source=tcBusiness&mode=signUp`
diff --git a/src/lib/functions/authentication-functions/authentication.functions.test.ts b/src-ts/lib/functions/authentication-functions/authentication.functions.test.ts
similarity index 100%
rename from src/lib/functions/authentication-functions/authentication.functions.test.ts
rename to src-ts/lib/functions/authentication-functions/authentication.functions.test.ts
diff --git a/src/lib/functions/authentication-functions/authentication.functions.ts b/src-ts/lib/functions/authentication-functions/authentication.functions.ts
similarity index 97%
rename from src/lib/functions/authentication-functions/authentication.functions.ts
rename to src-ts/lib/functions/authentication-functions/authentication.functions.ts
index f437f1ed4..33f686402 100644
--- a/src/lib/functions/authentication-functions/authentication.functions.ts
+++ b/src-ts/lib/functions/authentication-functions/authentication.functions.ts
@@ -1,8 +1,8 @@
import cookies from 'browser-cookies'
import { configureConnector, decodeToken, getFreshToken } from 'tc-auth-lib'
-import { User } from '../../../../types/tc-auth-lib'
import { EnvironmentConfig } from '../../../config'
+import { User } from '../../../types/tc-auth-lib'
import { logError } from '../logging-functions'
import { authentication as authenticationUrl } from './authentication-url.config'
@@ -21,7 +21,6 @@ configureConnector({
})
export async function initializeAsync(): Promise
{
-
return getFreshToken()
.then((tokenV3: string) => {
const tokenV2: string | null = cookies.get(CookieKeys.tcjwt)
diff --git a/src/lib/functions/authentication-functions/cookie-keys.enum.ts b/src-ts/lib/functions/authentication-functions/cookie-keys.enum.ts
similarity index 100%
rename from src/lib/functions/authentication-functions/cookie-keys.enum.ts
rename to src-ts/lib/functions/authentication-functions/cookie-keys.enum.ts
diff --git a/src/lib/functions/authentication-functions/index.ts b/src-ts/lib/functions/authentication-functions/index.ts
similarity index 100%
rename from src/lib/functions/authentication-functions/index.ts
rename to src-ts/lib/functions/authentication-functions/index.ts
diff --git a/src-ts/lib/functions/component-visible-functions/component-visible.functions.tsx b/src-ts/lib/functions/component-visible-functions/component-visible.functions.tsx
new file mode 100644
index 000000000..3d65a782b
--- /dev/null
+++ b/src-ts/lib/functions/component-visible-functions/component-visible.functions.tsx
@@ -0,0 +1,53 @@
+import { MouseEvent as RMouseEvent, MouseEventHandler, MutableRefObject, useCallback, useEffect, useRef } from 'react'
+
+/**
+ * Registers an outside click event handler, and calls the callback on click outside
+ * @param el Html element to register the event handler for
+ * @param cb Callback function to be called on click outside of provided element
+ */
+export function useClickOutside(el: HTMLElement | null, cb: (ev: MouseEvent) => void): void {
+ const handleClick: (ev: MouseEvent) => void = useCallback((ev: MouseEvent) => {
+ if (el && !el.contains(ev.target as unknown as Node)) {
+ cb(ev)
+ }
+ }, [cb, el])
+
+ useEffect(() => {
+ if (!el) {
+ document.removeEventListener('click', handleClick)
+ return
+ }
+
+ document.addEventListener('click', handleClick)
+ return () => {
+ document.removeEventListener('click', handleClick)
+ }
+ }, [el, handleClick])
+}
+
+export interface UseHoverElementValue {
+ onMouseEnter: MouseEventHandler
+ onMouseLeave: MouseEventHandler
+}
+
+/**
+ * Create event handlers for hover in/hover out for the passed element
+ * @param el Html element to register the event handlers for
+ * @param cb Callback function to be called on hover in/hover out of provided element
+ */
+export function useOnHoverElement(el: HTMLElement | null, cb: (isVisible: boolean) => void): UseHoverElementValue {
+ const counter: MutableRefObject = useRef(0)
+
+ const handleHover: (ev: RMouseEvent) => void = useCallback((ev: RMouseEvent) => {
+ const nextVal: number = Math.max(0, counter.current + (ev.type === 'mouseenter' ? 1 : -1))
+ if (!!nextVal !== !!counter.current) {
+ cb(nextVal > 0)
+ }
+ counter.current = nextVal
+ }, [cb, el])
+
+ return {
+ onMouseEnter: handleHover,
+ onMouseLeave: handleHover,
+ }
+}
diff --git a/src/lib/functions/component-visible-functions/index.ts b/src-ts/lib/functions/component-visible-functions/index.ts
similarity index 100%
rename from src/lib/functions/component-visible-functions/index.ts
rename to src-ts/lib/functions/component-visible-functions/index.ts
diff --git a/src/lib/functions/index.ts b/src-ts/lib/functions/index.ts
similarity index 87%
rename from src/lib/functions/index.ts
rename to src-ts/lib/functions/index.ts
index c2c032a5c..72a5da797 100644
--- a/src/lib/functions/index.ts
+++ b/src-ts/lib/functions/index.ts
@@ -6,5 +6,6 @@ export {
} from './authentication-functions'
export * from './component-visible-functions'
export * from './logging-functions'
+export * from './text-format-functions'
export * from './user-functions'
export * from './xhr-functions'
diff --git a/src/lib/functions/logging-functions/index.ts b/src-ts/lib/functions/logging-functions/index.ts
similarity index 100%
rename from src/lib/functions/logging-functions/index.ts
rename to src-ts/lib/functions/logging-functions/index.ts
diff --git a/src/lib/functions/logging-functions/logging.functions.test.ts b/src-ts/lib/functions/logging-functions/logging.functions.test.ts
similarity index 100%
rename from src/lib/functions/logging-functions/logging.functions.test.ts
rename to src-ts/lib/functions/logging-functions/logging.functions.test.ts
diff --git a/src/lib/functions/logging-functions/logging.functions.ts b/src-ts/lib/functions/logging-functions/logging.functions.ts
similarity index 100%
rename from src/lib/functions/logging-functions/logging.functions.ts
rename to src-ts/lib/functions/logging-functions/logging.functions.ts
diff --git a/src-ts/lib/functions/text-format-functions/index.ts b/src-ts/lib/functions/text-format-functions/index.ts
new file mode 100644
index 000000000..ef54240e8
--- /dev/null
+++ b/src-ts/lib/functions/text-format-functions/index.ts
@@ -0,0 +1,4 @@
+export {
+ dateLocaleShortString as textFormatDateLocaleShortString,
+ moneyLocaleString as textFormatMoneyLocaleString,
+} from './text-format.functions'
diff --git a/src-ts/lib/functions/text-format-functions/text-format.functions.ts b/src-ts/lib/functions/text-format-functions/text-format.functions.ts
new file mode 100644
index 000000000..10f33de2b
--- /dev/null
+++ b/src-ts/lib/functions/text-format-functions/text-format.functions.ts
@@ -0,0 +1,16 @@
+export function dateLocaleShortString(date?: Date): string | undefined {
+ return date?.toLocaleDateString(undefined,
+ {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ })
+}
+
+export function moneyLocaleString(amount?: number): string | undefined {
+ return amount?.toLocaleString('en-US', {
+ currency: 'USD', // TODO: handle other currencies
+ maximumFractionDigits: 0,
+ style: 'currency',
+ })
+}
diff --git a/src/lib/functions/token-functions/index.ts b/src-ts/lib/functions/token-functions/index.ts
similarity index 100%
rename from src/lib/functions/token-functions/index.ts
rename to src-ts/lib/functions/token-functions/index.ts
diff --git a/src/lib/functions/token-functions/token.functions.test.ts b/src-ts/lib/functions/token-functions/token.functions.test.ts
similarity index 100%
rename from src/lib/functions/token-functions/token.functions.test.ts
rename to src-ts/lib/functions/token-functions/token.functions.test.ts
diff --git a/src/lib/functions/token-functions/token.functions.ts b/src-ts/lib/functions/token-functions/token.functions.ts
similarity index 100%
rename from src/lib/functions/token-functions/token.functions.ts
rename to src-ts/lib/functions/token-functions/token.functions.ts
diff --git a/src/lib/functions/token-functions/token.model.ts b/src-ts/lib/functions/token-functions/token.model.ts
similarity index 100%
rename from src/lib/functions/token-functions/token.model.ts
rename to src-ts/lib/functions/token-functions/token.model.ts
diff --git a/src/lib/functions/user-functions/index.ts b/src-ts/lib/functions/user-functions/index.ts
similarity index 100%
rename from src/lib/functions/user-functions/index.ts
rename to src-ts/lib/functions/user-functions/index.ts
diff --git a/src/lib/functions/user-functions/user-store/index.ts b/src-ts/lib/functions/user-functions/user-store/index.ts
similarity index 100%
rename from src/lib/functions/user-functions/user-store/index.ts
rename to src-ts/lib/functions/user-functions/user-store/index.ts
diff --git a/src/lib/functions/user-functions/user-store/user-endpoint.config.ts b/src-ts/lib/functions/user-functions/user-store/user-endpoint.config.ts
similarity index 100%
rename from src/lib/functions/user-functions/user-store/user-endpoint.config.ts
rename to src-ts/lib/functions/user-functions/user-store/user-endpoint.config.ts
diff --git a/src/lib/functions/user-functions/user-store/user-xhr.store.ts b/src-ts/lib/functions/user-functions/user-store/user-xhr.store.ts
similarity index 100%
rename from src/lib/functions/user-functions/user-store/user-xhr.store.ts
rename to src-ts/lib/functions/user-functions/user-store/user-xhr.store.ts
diff --git a/src/lib/functions/user-functions/user.functions.test.ts b/src-ts/lib/functions/user-functions/user.functions.test.ts
similarity index 100%
rename from src/lib/functions/user-functions/user.functions.test.ts
rename to src-ts/lib/functions/user-functions/user.functions.test.ts
diff --git a/src/lib/functions/user-functions/user.functions.ts b/src-ts/lib/functions/user-functions/user.functions.ts
similarity index 100%
rename from src/lib/functions/user-functions/user.functions.ts
rename to src-ts/lib/functions/user-functions/user.functions.ts
diff --git a/src/lib/functions/xhr-functions/index.ts b/src-ts/lib/functions/xhr-functions/index.ts
similarity index 54%
rename from src/lib/functions/xhr-functions/index.ts
rename to src-ts/lib/functions/xhr-functions/index.ts
index 843643d90..42e673410 100644
--- a/src/lib/functions/xhr-functions/index.ts
+++ b/src-ts/lib/functions/xhr-functions/index.ts
@@ -1,5 +1,8 @@
export {
+ deleteAsync as xhrDeleteAsync,
getAsync as xhrGetAsync,
+ getBlobAsync as xhrGetBlobAsync,
patchAsync as xhrPatchAsync,
+ postAsync as xhrPostAsync,
putAsync as xhrPutAsync,
} from './xhr.functions'
diff --git a/src/lib/functions/xhr-functions/xhr.functions.test.ts b/src-ts/lib/functions/xhr-functions/xhr.functions.test.ts
similarity index 100%
rename from src/lib/functions/xhr-functions/xhr.functions.test.ts
rename to src-ts/lib/functions/xhr-functions/xhr.functions.test.ts
diff --git a/src/lib/functions/xhr-functions/xhr.functions.ts b/src-ts/lib/functions/xhr-functions/xhr.functions.ts
similarity index 72%
rename from src/lib/functions/xhr-functions/xhr.functions.ts
rename to src-ts/lib/functions/xhr-functions/xhr.functions.ts
index 6c37258f5..3c15f2a5d 100644
--- a/src/lib/functions/xhr-functions/xhr.functions.ts
+++ b/src-ts/lib/functions/xhr-functions/xhr.functions.ts
@@ -28,16 +28,31 @@ xhrInstance.interceptors.response.use((config) => config,
}
)
+export async function deleteAsync(url: string): Promise {
+ const output: AxiosResponse = await xhrInstance.delete(url)
+ return output.data
+}
+
export async function getAsync(url: string): Promise {
const output: AxiosResponse = await xhrInstance.get(url)
return output.data
}
+export async function getBlobAsync(url: string): Promise {
+ const output: AxiosResponse = await xhrInstance.get(url, { responseType: 'blob' })
+ return output.data
+}
+
export async function patchAsync(url: string, data: T): Promise {
const output: AxiosResponse = await xhrInstance.patch(url, data)
return output.data
}
+export async function postAsync(url: string, data: T): Promise {
+ const output: AxiosResponse = await xhrInstance.post(url, data)
+ return output.data
+}
+
export async function putAsync(url: string, data: T): Promise {
const output: AxiosResponse = await xhrInstance.put(url, data)
return output.data
diff --git a/src/lib/global-config.model.ts b/src-ts/lib/global-config.model.ts
similarity index 84%
rename from src/lib/global-config.model.ts
rename to src-ts/lib/global-config.model.ts
index 5427a8da8..41b3339d8 100644
--- a/src/lib/global-config.model.ts
+++ b/src-ts/lib/global-config.model.ts
@@ -1,5 +1,7 @@
export interface GlobalConfig {
API: {
+ FORUM_ACCESS_TOKEN: string
+ FORUM_V2: string
V3: string
V5: string
}
diff --git a/src-ts/lib/hooks/use-window-size.hook.ts b/src-ts/lib/hooks/use-window-size.hook.ts
new file mode 100644
index 000000000..df3944a28
--- /dev/null
+++ b/src-ts/lib/hooks/use-window-size.hook.ts
@@ -0,0 +1,34 @@
+import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'
+
+export interface WindowSize {
+ height: number
+ width: number
+}
+
+export function useWindowSize(): WindowSize {
+ const [windowSize, setWindowSize]: [WindowSize, Dispatch>] = useState({
+ height: 0,
+ width: 0,
+ })
+
+ const handleResize: () => void = useCallback(() => {
+ // Set window width/height to state
+ setWindowSize({
+ height: window.innerHeight,
+ width: window.innerWidth,
+ })
+ }, [])
+
+ useEffect(() => {
+ // Add event listener
+ window.addEventListener('resize', handleResize)
+
+ // Call handler right away so state gets updated with initial window size
+ handleResize()
+
+ // Remove event listener on cleanup
+ return () => window.removeEventListener('resize', handleResize)
+ }, [handleResize]) // Empty array ensures that effect is only run on mount
+
+ return windowSize
+}
diff --git a/src-ts/lib/index.ts b/src-ts/lib/index.ts
new file mode 100644
index 000000000..6ae9572e0
--- /dev/null
+++ b/src-ts/lib/index.ts
@@ -0,0 +1,36 @@
+export * from './avatar'
+export * from './breadcrumb'
+export * from './button'
+export * from './card'
+export * from './contact-support-form'
+export * from './content-layout'
+export * from './form'
+export * from './global-config.model'
+export {
+ analyticsInitialize,
+ authUrlLogin,
+ authUrlLogout,
+ authUrlSignup,
+ logInfo,
+ logInitialize,
+ textFormatDateLocaleShortString,
+ textFormatMoneyLocaleString,
+ useClickOutside,
+ useOnHoverElement,
+ xhrDeleteAsync,
+ xhrGetAsync,
+ xhrGetBlobAsync,
+ xhrPatchAsync,
+ xhrPostAsync,
+} from './functions'
+export * from './loading-spinner'
+export * from './modals'
+export * from './page-footer'
+export * from './pagination'
+export * from './portal'
+export * from './profile-provider'
+export * from './route-provider'
+export * from './svgs'
+export * from './table'
+export * from './tabs-navbar'
+export * from './tooltip'
diff --git a/src-ts/lib/loading-spinner/LoadingSpinner.module.scss b/src-ts/lib/loading-spinner/LoadingSpinner.module.scss
new file mode 100644
index 000000000..f8cbd50d1
--- /dev/null
+++ b/src-ts/lib/loading-spinner/LoadingSpinner.module.scss
@@ -0,0 +1,24 @@
+@import '../styles/variables';
+
+.loading-spinner {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background: $black-10;
+ color: $tc-white;
+ z-index: 1000;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: opacity 1s ease;
+ opacity: 1;
+ &.hide {
+ opacity: 0;
+ pointer-events: none;
+ }
+ &.show {
+ opacity: 1;
+ }
+}
diff --git a/src-ts/lib/loading-spinner/LoadingSpinner.tsx b/src-ts/lib/loading-spinner/LoadingSpinner.tsx
new file mode 100644
index 000000000..efb373d95
--- /dev/null
+++ b/src-ts/lib/loading-spinner/LoadingSpinner.tsx
@@ -0,0 +1,25 @@
+/**
+ * LoadingSpinner
+ *
+ * Centered Loading Spinner with back overlay
+ */
+import classNames from 'classnames'
+import { FC } from 'react'
+import { PuffLoader } from 'react-spinners'
+
+import styles from './LoadingSpinner.module.scss'
+
+export interface LoadingSpinnerProps {
+ className?: string
+ show?: boolean
+}
+
+const LoadingSpinner: FC = ({ show = false, className }: LoadingSpinnerProps) => {
+ return (
+
+ )
+}
+
+export default LoadingSpinner
diff --git a/src-ts/lib/loading-spinner/index.ts b/src-ts/lib/loading-spinner/index.ts
new file mode 100644
index 000000000..bfb1856af
--- /dev/null
+++ b/src-ts/lib/loading-spinner/index.ts
@@ -0,0 +1 @@
+export { default as LoadingSpinner } from './LoadingSpinner'
diff --git a/src-ts/lib/modals/base-modal/BaseModal.module.scss b/src-ts/lib/modals/base-modal/BaseModal.module.scss
new file mode 100644
index 000000000..a8241cfa2
--- /dev/null
+++ b/src-ts/lib/modals/base-modal/BaseModal.module.scss
@@ -0,0 +1,26 @@
+@import '../../styles/includes';
+@import '../../styles/typography';
+
+.modal-header {
+ padding: 5px 0 0;
+}
+
+.spacer {
+ margin: 24px 0;
+}
+
+.modal-body {
+ flex: 1 1 auto;
+ overflow: auto;
+ margin: 0 -1*$pad-xxxxl -1*$pad-xxxxl;
+ padding: 0 $pad-xxxxl $pad-xxxxl;
+
+ @extend .body-main;
+ & :global(.button-container) {
+ display: flex;
+ justify-content: flex-end;
+ gap: 16px;
+ align-items: center;
+ margin-top: 24px;
+ }
+}
diff --git a/src-ts/lib/modals/base-modal/BaseModal.tsx b/src-ts/lib/modals/base-modal/BaseModal.tsx
new file mode 100644
index 000000000..f26ef670d
--- /dev/null
+++ b/src-ts/lib/modals/base-modal/BaseModal.tsx
@@ -0,0 +1,39 @@
+import classNames from 'classnames'
+import { FC } from 'react'
+import Modal, { ModalProps } from 'react-responsive-modal'
+
+import { IconOutline } from '../../svgs'
+
+import styles from './BaseModal.module.scss'
+
+export interface BaseModalProps extends ModalProps {
+ size?: 'lg' | 'md'
+ title: string
+}
+
+const BaseModal: FC = ({
+ children,
+ title,
+ ...props
+}: BaseModalProps) => {
+
+ return (
+ }
+ >
+
+
{title}
+
+
+
+
+
+ {children}
+
+
+ )
+}
+
+export default BaseModal
diff --git a/src-ts/lib/modals/base-modal/index.ts b/src-ts/lib/modals/base-modal/index.ts
new file mode 100644
index 000000000..69e0ee8a1
--- /dev/null
+++ b/src-ts/lib/modals/base-modal/index.ts
@@ -0,0 +1 @@
+export { default as BaseModal } from './BaseModal'
diff --git a/src-ts/lib/modals/confirm/ConfirmModal.tsx b/src-ts/lib/modals/confirm/ConfirmModal.tsx
new file mode 100755
index 000000000..887e955de
--- /dev/null
+++ b/src-ts/lib/modals/confirm/ConfirmModal.tsx
@@ -0,0 +1,45 @@
+import { FC } from 'react'
+import { ModalProps } from 'react-responsive-modal'
+
+import { Button } from '../../button'
+import { BaseModal } from '../base-modal'
+
+export interface ConfirmModalProps extends ModalProps {
+ action?: string
+ onConfirm: () => void
+ title: string
+}
+
+const ConfirmModal: FC = ({
+ children,
+ onConfirm,
+ action = 'Confirm',
+ ...props
+}: ConfirmModalProps) => {
+ return (
+
+ {children}
+
+
+
+
+
+ )
+}
+
+export default ConfirmModal
diff --git a/src-ts/lib/modals/confirm/index.ts b/src-ts/lib/modals/confirm/index.ts
new file mode 100644
index 000000000..b00ce52b7
--- /dev/null
+++ b/src-ts/lib/modals/confirm/index.ts
@@ -0,0 +1 @@
+export { default as ConfirmModal } from './ConfirmModal'
diff --git a/src-ts/lib/modals/contact-support-modal/ContactSupportModal.tsx b/src-ts/lib/modals/contact-support-modal/ContactSupportModal.tsx
new file mode 100644
index 000000000..bb14da1b1
--- /dev/null
+++ b/src-ts/lib/modals/contact-support-modal/ContactSupportModal.tsx
@@ -0,0 +1,39 @@
+import { Dispatch, FC, SetStateAction, useState } from 'react'
+
+import { ContactSupportForm, contactSupportFormDef } from '../../contact-support-form'
+import { FormDefinition, formOnReset } from '../../form'
+import { BaseModal } from '../base-modal'
+
+export interface ContactSupportModal {
+ isOpen: boolean
+ onClose: () => void
+ workId?: string
+}
+
+const ContactSupportModal: FC = (props: ContactSupportModal) => {
+
+ const [formDef, setFormDef]: [FormDefinition, Dispatch>] = useState({ ...contactSupportFormDef })
+
+ function onClose(): void {
+ const updatedForm: FormDefinition = { ...formDef }
+ formOnReset(updatedForm.inputs)
+ setFormDef(updatedForm)
+ props.onClose()
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default ContactSupportModal
diff --git a/src-ts/lib/modals/contact-support-modal/index.ts b/src-ts/lib/modals/contact-support-modal/index.ts
new file mode 100644
index 000000000..7867c7ee8
--- /dev/null
+++ b/src-ts/lib/modals/contact-support-modal/index.ts
@@ -0,0 +1 @@
+export { default as ContactSupportModal } from './ContactSupportModal'
diff --git a/src-ts/lib/modals/index.ts b/src-ts/lib/modals/index.ts
new file mode 100644
index 000000000..92fad71c2
--- /dev/null
+++ b/src-ts/lib/modals/index.ts
@@ -0,0 +1,6 @@
+export * from './base-modal'
+export * from './confirm'
+export * from './contact-support-modal'
+export * from './order-contract-modal'
+export * from './privacy-policy-modal'
+export * from './terms-modal'
diff --git a/src-ts/lib/modals/order-contract-modal/OrderContractModal.module.scss b/src-ts/lib/modals/order-contract-modal/OrderContractModal.module.scss
new file mode 100644
index 000000000..a3c1fffa7
--- /dev/null
+++ b/src-ts/lib/modals/order-contract-modal/OrderContractModal.module.scss
@@ -0,0 +1,25 @@
+@import '../../styles/includes';
+
+.container {
+
+ ol {
+ padding: 0;
+ }
+
+ h4 {
+ margin-top: 20px;
+ }
+
+ p {
+ margin-bottom: 20px;
+
+ &.sm {
+ font-size: 14px;
+ }
+ }
+}
+
+.topCoderLink {
+ text-decoration: underline;
+ color: $link-blue-light;
+}
diff --git a/src-ts/lib/modals/order-contract-modal/OrderContractModal.tsx b/src-ts/lib/modals/order-contract-modal/OrderContractModal.tsx
new file mode 100644
index 000000000..d0bcc5203
--- /dev/null
+++ b/src-ts/lib/modals/order-contract-modal/OrderContractModal.tsx
@@ -0,0 +1,635 @@
+import { FC } from 'react'
+
+import { BaseModal } from '../base-modal'
+
+import styles from './OrderContractModal.module.scss'
+
+export interface OrderContractModal {
+ isOpen: boolean
+ onClose: () => void
+}
+
+const OrderContractModal: FC = ({ isOpen, onClose }: OrderContractModal) => (
+
+
+
Date of last revision: January 28, 2022
+
+ This Topcoder Online Order User Agreement (the "Agreement") is a contract
+ between you (referred to herein as "User") and Topcoder LLC. ("Topcoder")
+ and applies to all online services available therein (the "Services") and
+ Deliverables. By submitting an Online Order through{' '}
+
+ https://www.topcoder.com/{' '}
+ {' '}
+ and related sub-domains (“the Site”) , User has accepted this Agreement
+ and has agreed to be bound by the terms of this Agreement. Topcoder
+ reserves the right, at its discretion, to amend this Agreement at any time
+ by posting a revised version on the Site without notice. The revised
+ version will be effective from the time Topcoder posts it. User’s
+ continued use of the Services after the amendment to the Agreement shall
+ signify User’s understanding and acceptance of the amended Agreement.
+ User’s usage and viewing of the Site shall be governed by Topcoder Online
+ Customer Terms of Use Agreement that the User has agreed to while
+ registering on the Site, which is incorporated herein through reference.
+
+
+ -
+
1. SERVICES AND SPECIFICATIONS
+
+ -
+
+ 1.1. Process and Specifications for Online
+ Orders. If ordering the Services online via the Site (referred to
+ as an “Online Order”):
+
+
+ -
+ a. To utilize the Services, the Site requires
+ the User to provide specifications and information with respect
+ to the desired Services and deliverables in reasonable detail
+ (the "Preliminary Specifications") on the Site.
+
+ -
+ b. After submitting Preliminary Specifications
+ on the Site, User provides payment information and authorizes
+ Project Fees (as defined below) associated with the Services and
+ deliverables.
+
+ -
+ c . Topcoder may in its sole discretion accept
+ or refer Preliminary Specifications to a Topcoder representative
+ for adjustments in the Preliminary Specifications to be
+ acceptable to Topcoder or reject the Preliminary Specifications.
+ Preliminary Specifications accepted by Topcoder will be referred
+ to as “Specifications” and result in an Online
+ Order.
+
+ -
+ d. To the extent the parties are not able to
+ reach agreement on the Specifications, including, but not
+ limited to situations where the information provided by User is
+ insufficient to create Specifications or the Preliminary
+ Specifications are outside the scope of the services described
+ on the Site, Topcoder shall not charge User any Project Fees and
+ Topcoder shall have no further liability or obligations under
+ this Agreement.
+
+ -
+ e. Topcoder agrees to provide the Deliverables
+ as per the Specifications. Deliverables are provided by
+ launching Work (as defined below) using Topcoder's crowdsourcing
+ platform, available at the Site and online and mobile tools for
+ engaging the Topcoder Community (the “Platform
+ ”).
+
+
+
+ 1.2. Changes to Scope of Services. If User
+ desires to change a Specification, User will submit a written
+ request to Topcoder detailing the proposed changes. If Topcoder is
+ able to accommodate such changes, Topcoder shall prepare an
+ amendment to the Specifications and/or Order Form detailing the
+ changes, any fee adjustments required as a result of such changes,
+ any adjustments to the delivery schedule required as a result of
+ such changes, and any other adjustments (a “Change Order”). If the
+ Change Order is agreeable to User, both parties will execute the
+ Change Order.
+
+ 2. DEFINITIONS
+
+
+ 2.1. For the purposes of this Agreement, the
+ following capitalized terms have the meanings assigned to them
+ in this Section 2. Any capitalized terms used in this Agreement
+ but not otherwise defined in this Section shall have the
+ meanings assigned to them elsewhere in this Agreement.
+
+ -
+ a. “Deliverables”: shall mean
+ those items described and itemized in the Specifications.
+
+ -
+ b. “Project Fees”: The fees
+ User needs to pay for the Services and the creation of the
+ Deliverables as either specified on the Site or as may be
+ mutually agreed between the parties in the Online Order. Project
+ Fees are on a fixed price basis.
+
+ -
+ c. “Work”: Services and/or
+ Deliverables provided through the Platform (as defined below)
+ utilizing the Topcoder Community. Work may also be referred to
+ as “Challenges”, “Tasks”, "Competitions" or "Contests”. Work is
+ primarily enabled through the following: “
+ Challenge”: A fixed-price, outcome-based online
+ competition on the Platform utilizing the Topcoder Community.
+ Also referred to as Competitions or Tasks. Work may result in
+ multiple deliverables from the Topcoder Community (“
+ Submissions”). However, not all Submissions
+ shall be considered as Deliverables. Deliverables shall solely
+ be determined and provided to User in accordance with criteria
+ defined in Specifications after being selected through
+ Topcoder’s deliverable review and scoring processes.
+
+ -
+ d. “Effective Date”: For
+ Online Orders, the Effective Date is the date that Preliminary
+ Specifications are submitted by User.
+
+ -
+ e. “Topcoder Community”:
+ Members of Topcoder’s global community who are registered
+ members on the Platform.
+
+
+ 3. PLATFORM USAGE
+
+ -
+ Utilization of the Site allows User to leverage the Topcoder
+ Community for the purchase/procurement of Services and
+ Deliverables. To the extent User desires to access the Topcoder
+ Platform in ways not provided by the Site, User may enter into a
+ separate agreement with Topcoder to purchase Work and/or a
+ subscription for such services. User may contact Topcoder via
+ email at support@topcoder.com for more details about
+ subscription pricing and fees associated with Work when utilized
+ with a subscription. Under no scenario shall the Platform and
+ tools/documentation related to the Platform be deemed to be a
+ Deliverable.
+
+
+ 4. TITLE AND OWNERSHIP
+
+ -
+ 4.1 Retained Rights. Nothing
+ in the Agreement is intended to grant User any right to any of
+ Topcoder’s intellectual property, including without limitation,
+ any of its know how, works in any media, software, information,
+ trade secrets, materials, property or proprietary interest,
+ trademarks, copyrights or logos (“Retained Rights”).
+
+ -
+ 4.2 Title to the Platform. Topcoder retains all
+ right, title and interest in and to the Platform, tools and
+ associated documentation and materials. Any use of the Platform,
+ tools or associated documentation or materials beyond the scope
+ of the rights expressly granted in this Agreement is prohibited
+ and shall constitute a material breach of this Agreement,
+ pursuant to which Topcoder may immediately terminate this
+ Agreement and any work-in-progress. User shall retain Topcoder's
+ copyright notices and authorship credits in the Platform, tools
+ and associated documentation or materials.
+
+ -
+ 4.3. Deliverables. Regardless of Work
+ methodology, Deliverables shall be provided in accordance with
+ criteria defined in Specifications, the selected Services, and
+ Topcoder’s standard deliverable review and scoring processes.
+ Unless otherwise agreed to by the parties pursuant to this
+ Section 4.3, User acknowledges and agrees that it will obtain
+ rights solely in and to the Deliverables, as further described
+ in Section 7, and no other Submissions from Work. Subject to the
+ terms of this Agreement, and upon payment in full of the Project
+ Fees, Topcoder assigns and will assign to User the intellectual
+ property rights in Deliverables created for the User pursuant to
+ any Online Order which is expressly designated as being for
+ "development work" (“Custom Software”).
+ Deliverables shall be deemed to have been accepted upon User’s
+ receipt of the Deliverables. The rights in and to any
+ Submissions other than the Deliverables shall be retained by the
+ member of the Topcoder Community who submitted such a
+ Submission. For the purposes of this Agreement, such Submissions
+ shall be deemed to be Topcoder’s Confidential Information. Upon
+ mutual agreement of the parties, User may purchase more than one
+ (1) Submission and Topcoder shall invoice User for such
+ additional payment necessary to purchase such additional
+ Submission(s), and upon Topcoder's receipt of such payment, such
+ purchased additional Submission(s) shall also be deemed to be
+ Deliverable(s). In accordance with above terms, User owns and
+ will retain ownership (including all intellectual property
+ rights) in and to the Deliverables, and Topcoder will assign and
+ does hereby assign all rights, title and interests in the
+ Deliverables to User. For the avoidance of doubt, the parties
+ acknowledge that to the extent the Deliverables consist of
+ software applications designed to be operated on or accessed
+ through a third- party platform (such as, including not limited
+ to, Amazon, Salesforce.com, Workday, or Google), then User is
+ solely responsible for obtaining license rights to access and
+ utilize such platforms in relation to using the Deliverables.
+
+
+ 5. PAYMENT
+
+ -
+ 5.1 All Project Fees shall be placed as a hold
+ on User’s debit, credit card or other payment means after the
+ Preliminary Specifications are submitted and Project Fees are
+ authorized by User. Upon Topcoder agreement to the Preliminary
+ Specification (resulting in Specification & Online Order), User
+ shall be charged the total Project Fees. If Topcoder refers User
+ to a Topcoder representative, the hold shall stand as cancelled.
+ User is responsible for paying all Project Fees in a timely
+ manner via a valid and authorized payment method for all Project
+ Fees. The parties understand and acknowledge that Topcoder is
+ under no obligation to provide any Services or deliver any
+ Deliverables until the Project Fees are paid in full, using the
+ payment method Topcoder determines to be acceptable as described
+ on the Site. User agrees that Topcoder may charge the payment
+ method provided by User for all Project Fees. User represents
+ and warrants that User has the legal right to use any debit or
+ credit cards or other payment means used to initiate any
+ transaction. User agrees to pay all charges incurred by User at
+ the prices in effect when such charges are incurred. User will
+ also be responsible for paying any applicable taxes relating to
+ User's purchase of Services and/or Deliverables. Except as
+ explicitly set forth in Section 1 or Section 10.2, all Project
+ Fees are final and non- refundable. Topcoder does not have
+ access to any User’s debit, credit or bank information. The
+ payment and related processing shall be governed by the terms
+ and conditions of a reputable third party payment processor
+ appointed by Topcoder (which can be changed by Topcoder anytime
+ at its will without any requirement of a notification). Topcoder
+ cannot be held liable in any manner for a dispute or issue
+ arising out of or related to payment processing.
+
+
+ 6. TERM AND TERMINATION
+
+ -
+ 6.1 This Agreement will begin on the Effective
+ Date and will continue unless terminated as set forth in this
+ Agreement ("Term"). Topcoder may, in its sole
+ discretion, terminate this Agreement and/or any Online Order(s),
+ at any time, upon prior written notice to the User, which will
+ result in the termination of associated rights granted to the
+ User under this Agreement and/or Online Order as applicable. The
+ sole and exclusive remedy provided to User is specified in
+ Section 6.4.
+
+ -
+ 6.2 Either party may terminate this Agreement
+ and/or any Online Order(s) if the other party: (a) fails to cure
+ any material breach of this Agreement or the applicable Online
+ Order(s) within thirty (30) days after receipt of the written
+ notice of such breach; (b) ceases operation without a successor;
+ or (c) seeks protection under any bankruptcy or comparable
+ proceeding, or if any such proceeding is instituted against such
+ party (and not dismissed within sixty (60) days thereafter).
+
+ -
+ 6.3 Section 5 (Payment), Section 6 (Term and
+ Termination), Section 8 (Confidential Information), Section 9
+ (Limitation of Liability), and Section 11 (General) of this
+ Agreement shall survive any termination or expiration hereof.
+
+ -
+ 6.4 In the event of a termination of an Online
+ Order: (a) an User’s right to avail the Services under such
+ Online Order is automatically revoked; and ; (b) Topcoder shall
+ refund the Project Fees on a pro rata basis unless the
+ termination is due to User’s breach of this Agreement and/or
+ Online Order, in which case Topcoder shall not be obligated to
+ any refund.
+
+ -
+ 6.5 In the event of a termination of the
+ Agreement before all Online Orders executed hereunder are
+ terminated or completed, Topcoder will continue to perform and
+ deliver Services for which the User has already paid and the
+ terms of this Agreement shall remain in full force until the
+ termination or completion of such Online Orders.
+
+
+
+ 7. THIRD PARTY SOFTWARE AND EXPORT/IMPORT COMPLIANCE
+
+
+ -
+ 7.1. Work. Topcoder will conduct Work for User
+ on the Platform among members of the Topcoder Community using
+ the Platform tools to provide the Services. Topcoder shall have
+ final control and ownership over all Work documents (e.g.,
+ specifications, contest rules and requirements, prizes, etc.).
+
+ -
+ 7.2. Third-Party Software. If the
+ Specifications (as mutually agreed by the parties) provide for
+ the Deliverables to interface with, accompany, or include
+ software or material not developed by Topcoder or its
+ affiliates, including any open source software ("Third-Party
+ Software"), any such Third-Party Software shall be subject to
+ its own terms and conditions, and shall not be considered
+ included as the part of the Deliverable under this Agreement.
+ Third-Party Software shall accompany or be included in
+ Deliverables only with User's permission. User shall bear all
+ license fees and other expenses, if any, applicable to such
+ Third-Party Software.
+
+ -
+ 7.3. Export Compliance. Regardless of whether
+ User is a U.S.-based entity, User shall not export or re-export
+ any of the Deliverable, (in whole or in part) to any country
+ without ensuring that such export complies with the Export
+ Administration Regulations of the U.S. Department of Commerce,
+ and any other applicable statute, regulation, or government
+ order. User warrants that it is not named on any restricted or
+ denied party list pursuant to any embargo, sanction, debarment
+ or denied party designation maintained by any country or
+ government, including without limitation U.S. government whose
+ laws are applicable to this Agreement.
+
+
+ 8. CONFIDENTIAL INFORMATION
+
+
+ -
+ 8.1. Confidential Information. "Confidential
+ Information" means information which is provided by either party
+ under this Agreement (in such capacity, the "Disclosing Party") to
+ the other party (in such capacity, the "Receiving Party") which is
+ marked as “confidential,” proprietary” or some similar indication;
+ (ii) is expressly advised by the Disclosing Party to be
+ confidential through some contemporaneous written means; or (iii)
+ which the Receiving Party would reasonably construe to be
+ confidential information under the circumstances. Confidential
+ Information may include information that is proprietary, trade
+ secret and/or confidential, including, but not limited to,
+ techniques, designs, specifications, drawings, blueprints,
+ tracings, diagrams, models, samples, flow charts, data, computer
+ programs, disks, diskettes, tapes, business plans, marketing
+ plans, User names and other technical, financial or commercial
+ information and intellectual property. For the avoidance of doubt,
+ notwithstanding anything to the contrary, the Platform, tools and
+ associated documentation or materials, and any other information
+ that Topcoder provides to User hereunder that should reasonably be
+ known to be confidential, shall constitute Topcoder's Confidential
+ Information. Expect as expressly authorized herein, the Receiving
+ Party will (i) hold in confidence and not use or disclose any
+ Confidential Information using the same degree of care that it
+ uses to safeguard its own confidential materials or data of
+ similar nature; and (ii) limit dissemination of such Confidential
+ Information to persons within the Receiving Party's business
+ organization who have a need to receive such Confidential
+ Information.
+
+ -
+ 8.2. Exceptions. These confidentiality
+ obligations shall not apply to any Confidential Information which:
+ (a) is generally known to the public at the time of disclosure or
+ later becomes so generally known (including Confidential
+ Information which is disclosed as part of Work); (b) is
+ subsequently learned from a third party without a duty of
+ confidentiality; (c) at the time of disclosure was already in the
+ possession of the Receiving Party; (d) was developed by employees
+ or agents of the Receiving Party independently of and without
+ reference to any Confidential Information of the Disclosing Party;
+ and (e) is required by law, court order or a governmental agency
+ to be disclosed.
+
+ -
+
+ 8.3. Title and License to User's Confidential Information.
+ {' '}
+ All right, title, and ownership to Confidential Information
+ provided by User hereunder, including the Preliminary
+ Specifications, remains with User. User represents that it has all
+ rights in the Confidential Information 6 necessary to include it
+ in Work. User hereby grants to Topcoder a license to use such
+ Confidential Information provided by User solely for the purposes
+ of providing the Services, including conducting the applicable
+ Work and providing the Deliverable.
+
+ -
+ 8.4. Equitable Relief. The Receiving Party
+ acknowledges that unauthorized disclosure of Confidential
+ Information would cause substantial harm for which damages alone
+ may not be a sufficient remedy, and therefore that upon any such
+ disclosure by the Receiving Party the Disclosing Party shall be
+ entitled to seek appropriate equitable relief in addition to
+ whatever other remedies it might have at law.
+
+ -
+ 8.5. Confidentiality on the Platform. User
+ acknowledges and agrees that members of the Topcoder Community are
+ not bound by the confidentiality obligations under this Section 8
+ and User acknowledges that any Specifications or other information
+ that the Topcoder Community is provided in connection with the
+ Services (except for personally identifiable information or
+ payment information) is not Confidential Information.
+
+ -
+ 8.6. Confidentiality Period: The terms of this
+ Section 8 shall continue in full force and effect for a period of
+ three (3) years from the date of disclosure of such Confidential
+ Information.
+
+ -
+ 8.7. Effect of Termination: In the event of
+ termination of this Agreement, upon written request of the
+ Disclosing Party, the Receiving Party shall promptly return the
+ Disclosing Party’s Confidential Information, or at the Disclosing
+ Party’s option destroy any remaining Confidential Information and
+ certify that such destruction has taken place, provided however
+ that Topcoder may retain a minimum of one copy of all Deliverable
+ and relevant project documentation for archival and audit
+ purposes. This does not apply to Confidential Information or
+ copies thereof which must be stored by the Receiving Party
+ according to law, provided that such Confidential Information or
+ copies thereof shall be subject to an indefinite confidentiality
+ obligation.
+
+
+ 9. LIMITATION OF LIABILITY
+
+ -
+ NEITHER PARTY WILL BE LIABLE FOR ANY LOSS OF USE, INTERRUPTION OF
+ BUSINESS, LOST PROFITS, OR ANY INDIRECT, EXEMPLARY, PUNITIVE,
+ SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY KIND
+ REGARDLESS OF THE FORM OF ACTION WHETHER IN CONTRACT, TORT
+ (INCLUDING NEGLIGENCE), STRICT PRODUCT LIABILITY, OR OTHERWISE,
+ EVEN IF IT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN
+ NO EVENT SHALL TOPCODER'S AGGREGATE LIABILITY TO USER EXCEED THE
+ TOTAL PROJECT FEES PAID BY USER TO TOPCODER. THE EXISTENCE OF ONE
+ OR MORE CLAIMS WILL NOT ENLARGE THIS LIMIT. THE PARTIES AGREE THAT
+ THE LIMITATIONS SPECIFIED IN THIS SECTION 9 WILL SURVIVE AND APPLY
+ EVEN IF ANY LIMITED REMEDY SPECIFIED IN THIS AGREEMENT IS FOUND TO
+ HAVE FAILED OF ITS ESSENTIAL PURPOSE. BOTH PARTIES WILL IN ALL
+ CIRCUMSTANCES USE THEIR BEST ENDEAVORS TO MITIGATE ANY LOSSES
+ WHICH ARE SAID TO ARISE BY REASON OF THE BREACH, NEGLIGENCE OR
+ OTHER DEFAULT ON THE PART OF THE OTHER PARTY.
+
+
+ 10. WARRANTY
+
+ -
+ 10.1. Representation. User represents and
+ warrants that (i) it has the requisite power and corporate
+ approvals and authority to enter into this Agreement and to
+ perform all of its obligations under this Agreement; (ii) it will
+ comply with all applicable laws, rules, and regulations, including
+ without limitation, all applicable data protection, privacy and
+ intellectual property laws in their conduct pursuant to this
+ Agreement and relating to its use of Site, Services and
+ Deliverables; (iii) it will comply with all the policies and
+ procedures that may be published on the Site from time to time
+ (including without limitation, Privacy Policy) which shall be
+ construed to form a part of the Agreement; and (iv) it shall
+ provide accurate, current and complete information while creating
+ a profile or registering an account on the Site and shall not
+ upload, transmit or otherwise provide access or make available any
+ information that contains personally identifiable information of
+ others, including without limitation, names, addresses, identity
+ numbers or phone numbers. Further, User represents that it shall
+ update its account information on an ongoing basis to ensure
+ completeness and accuracy of information.
+
+ -
+ 10.2. Limited Warranty. For a period of ten (10)
+ days from the provision of the Deliverables to the User, Topcoder
+ warrants that the Deliverables will materially conform to the
+ Specifications. If the Deliverables do not materially conform to
+ the Specifications, User shall, within ten (10) days from the
+ provision of the Deliverables to the User, notify Topcoder in
+ writing of and adequately describe any such non- conformance.
+ User's exclusive remedy and Topcoder's sole obligation shall be
+ to, at Topcoder's sole discretion, either (1) investigate the
+ errors and use commercially reasonable efforts to bring the
+ Deliverables into material conformance with the Specifications; or
+ (2) refund or credit the Project Fees paid for those Deliverables.
+ The warranty set forth in this section does not apply to
+ Deliverables that have been modified, damaged or operated contrary
+ to the Specifications.
+
+ -
+ 10.3. Disclaimer. THE FOREGOING WARRANTIES ARE
+ EXCLUSIVE REMEDIES AND ARE IN LIEU OF ALL OTHER REPRESENTATIONS OR
+ WARRANTIES, EXPRESS OR IMPLIED, STATUTORY OR OTHERWISE RELATED TO
+ THIS AGREEMENT, INCLUDING THE SITE, THE SERVICES AND THE
+ DELIVERABLES, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF
+ MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR
+ NONINFRINGEMENT. TOPCODER MAKES NO OTHER WARRANTY OF ANY KIND TO
+ USER OR TO ANY OTHER PARTY. THE PARTIES ACKNOWLEDGE AND AGREE THAT
+ THE PROJECT FEES CHARGED BY TOPCODER UNDER THIS AGREEMENT REFLECT
+ THE ALLOCATION OF RISKS PROVIDED BY THE FOREGOING WARRANTY, THE
+ LIMITATIONS OF LIABILITY, AND OTHER TERMS SET FORTH IN THIS
+ AGREEMENT, AND ANY MODIFICATION OF THE ALLOCATION OF RISKS WOULD
+ AFFECT THE PROJECT FEES CHARGED.
+
+
+ 11. GENERAL
+
+ -
+ 11.1. Each party is an independent contractor of
+ the other and neither is an employee, agent, partner or joint
+ venturer of the other.
+
+ -
+ 11.2. For the duration of this Agreement and for
+ twelve (12) months thereafter, User shall not solicit for
+ employment any persons employed or otherwise engaged by Topcoder,
+ whether or not such individual had direct interaction with User;
+ the foregoing restriction includes, but is not limited to, the
+ Topcoder Community, persons who are Topcoder employees,
+ contractors or agents.
+
+ -
+ 11.3. Neither party shall make any commitment, by
+ contract or otherwise, binding upon the other or represent that it
+ has any authority to do so. This Agreement will bind and inure to
+ the benefit of each party's permitted successors and assigns.
+ Neither party shall assign this Agreement without the advance
+ written consent of the other party, except that Topcoder may
+ assign this Agreement to an affiliate or in connection with a
+ merger, reorganization, acquisition or other transfer of all or
+ part of Topcoder's assets or voting securities.
+
+ -
+ 11.4. Any notice, report, approval or consent
+ required or permitted under this Agreement will be sent to
+ Topcoder at c/o Appirio Inc., 201 South Capitol Avenue Ste. 1100,
+ Indianapolis, Indiana 46225, Attention: General Counsel, Email:
+ gc@appirio.com or to User at the contact information provided by
+ User through the Site or in an Order Form.
+
+ -
+ 11.5. Any waiver by either party of any breach of
+ this Agreement, whether express or implied, will not constitute a
+ waiver of any other or subsequent breach. No provision of the
+ Agreement will be waived by any act, omission or knowledge of a
+ party or its agents or employees except by an instrument in
+ writing expressly waiving such provision and signed by a duly
+ authorized officer of the waiving party. If any provision of this
+ Agreement is adjudged by any court of competent jurisdiction to be
+ unenforceable or invalid, that provision shall be limited or
+ eliminated to the minimum extent necessary so that this Agreement
+ will otherwise remain in full force and effect.
+
+ -
+ 11.6. Neither party shall be liable to the other
+ for any delay of failure to perform any obligation under this
+ Agreement (except for a failure to pay fees) if the delay or
+ failure is due to events which are beyond the reasonable control
+ of such party, including but not limited to any strike, blockade,
+ government actions, war, act of terrorism, riot, natural disaster,
+ failure or diminishment of power or of telecommunications or data
+ networks or services, cyber-attack, or refusal of approval or a
+ license by a government agency.
+
+ -
+ 11.7. Topcoder may refer to User by name as a
+ customer in sales presentations and on Topcoder’s website and
+ other marketing material. Topcoder may develop case studies for
+ marketing purposes on User’s usage of Topcoder and share publicly
+ notifying User.
+
+ -
+ 11.8. This Agreement will be deemed to have been
+ made in, and shall be construed pursuant to the laws of the State
+ of Delaware without regard to its conflicts of laws, provisions,
+ or the United Nations Convention on International Sale of Goods.
+ The jurisdiction and venue for actions related to this Agreement
+ shall be the state and federal courts located in Wilmington
+ County, Delaware and both parties hereby submit to the personal
+ jurisdiction of such courts. Both parties hereby waive their right
+ to a trial by jury.
+
+ -
+ 11.9. Any waivers or amendments shall be
+ effective only if made in writing signed by a representative of
+ the respective parties authorized to bind the parties. No
+ provision of any purchase order or other business form (including
+ but not limited to security access forms of any kind) employed by
+ either party will supersede the terms and conditions of this
+ Agreement, and any such document shall be for administrative
+ purposes only and shall have no legal effect. In the event of any
+ inconsistency between this Agreement and the privacy policy on the
+ Site (“Privacy Policy”), the terms of this
+ Privacy Policy shall control. Both parties agree that this
+ Agreement is the complete and exclusive statement of the mutual
+ understanding of the parties, and supersedes and cancels all
+ previous written and oral agreements and communications relating
+ to the subject matter of this Agreement. In the event of any
+ inconsistency between this Agreement and the Topcoder Online
+ Customer Terms of Use Agreement, the one more onerous shall
+ control.
+
+
+
+
+
+
+
+
+)
+
+export default OrderContractModal
diff --git a/src-ts/lib/modals/order-contract-modal/index.ts b/src-ts/lib/modals/order-contract-modal/index.ts
new file mode 100644
index 000000000..0f8cb165a
--- /dev/null
+++ b/src-ts/lib/modals/order-contract-modal/index.ts
@@ -0,0 +1 @@
+export { default as OrderContractModal } from './OrderContractModal'
diff --git a/src-ts/lib/modals/privacy-policy-modal/PrivacyPolicyModal.module.scss b/src-ts/lib/modals/privacy-policy-modal/PrivacyPolicyModal.module.scss
new file mode 100644
index 000000000..37dd6272f
--- /dev/null
+++ b/src-ts/lib/modals/privacy-policy-modal/PrivacyPolicyModal.module.scss
@@ -0,0 +1,77 @@
+@import '../../styles/includes';
+
+.container {
+ line-height: 20px;
+
+ ol {
+ padding: 0;
+ list-style: none;
+ }
+
+ h4 {
+ margin: 20px 0;
+ }
+
+ p {
+ margin-bottom: 20px;
+
+ &.sm {
+ font-size: 14px;
+ }
+ }
+
+ ul.a {
+ list-style-type: disc;
+ list-style: disc;
+
+ li {
+ padding-left: 0;
+ margin-left: 20px;
+ }
+ }
+
+ ul.b {
+ list-style-type: disc;
+ list-style: disc;
+
+ li {
+ padding-left: 0;
+ margin-left: 20px;
+ }
+ }
+
+ ul.c {
+ list-style-type: disc;
+ list-style: disc;
+
+ li {
+ padding-left: 0;
+ margin-left: 20px;
+ }
+ }
+
+ ul.d {
+ list-style-type: disc;
+ list-style: disc;
+
+ li {
+ padding-left: 0;
+ margin-left: 20px;
+ }
+ }
+
+ ul.e {
+ list-style-type: disc;
+ list-style: disc;
+
+ li {
+ padding-left: 0;
+ margin-left: 20px;
+ }
+ }
+
+ a {
+ text-decoration: underline;
+ color: $link-blue-light;
+ }
+}
diff --git a/src-ts/lib/modals/privacy-policy-modal/PrivacyPolicyModal.tsx b/src-ts/lib/modals/privacy-policy-modal/PrivacyPolicyModal.tsx
new file mode 100644
index 000000000..cd736a411
--- /dev/null
+++ b/src-ts/lib/modals/privacy-policy-modal/PrivacyPolicyModal.tsx
@@ -0,0 +1,996 @@
+import { Dispatch, FC, MouseEvent, SetStateAction, useState } from 'react'
+
+import { ProfileProvider } from '../../profile-provider'
+import { BaseModal } from '../base-modal'
+import { ContactSupportModal } from '../contact-support-modal'
+
+import styles from './PrivacyPolicyModal.module.scss'
+
+export interface PrivacyPolicyModal {
+ isOpen: boolean
+ onClose: () => void
+}
+
+const PrivacyPolicyModal: FC = ({ isOpen, onClose }: PrivacyPolicyModal) => {
+
+ const [isSupportOpen, setIsSupportOpen]: [boolean, Dispatch>] = useState(false)
+
+ function openSupportModal(event: MouseEvent): void {
+ event.preventDefault()
+ setIsSupportOpen(true)
+ }
+
+ return (
+ <>
+
+ setIsSupportOpen(false)}
+ />
+
+
+
+
+
+ -
+
1. PROCESSING ACTIVITIES COVERED
+
+ -
+ This Privacy Policy explains our privacy practices for our website,
+ applications, the Topcoder platform, online marketing practices, and
+ other products or services (collectively, “Services”) owned and
+ operated by Topcoder, LLC or any of its subsidiaries (“Topcoder”).
+ Through use of the Services, you expressly consent to our
+ collection, use, disclosure, and retention of your personal
+ information and other personal information as described in this
+ Privacy Policy.
+
+
+ 2. WHAT PERSONAL DATA DO WE COLLECT?
+
+ -
+ 2.1 PERSONAL DATA WE COLLECT DIRECTLY FROM YOU
+
+
+ -
+ The Personal Data we collect directly from you includes identifiers,
+ professional or employment related information, financial account
+ information, commercial information, visual information, and
+ internet activity information. We collect such information in the
+ following situations:
+
+
+ -
+ If you register on the platform we require that you provide contact
+ information, such as name, email address, and country.
+
+ -
+ If you complete the user profile on the platform after you
+ register, you may voluntarily submit information such as contact
+ information, photographs, user preferences, communications
+ preferences, other biographical information, such as your
+ occupation, location, educational background, hobbies, social
+ media profiles, company name, areas of expertise and interests.
+
+
+ -
+ If you register for a competition or task on the platform we
+ collect information specific to the process and display the
+ time, date, and handle associated with your account.
+
+
+ -
+ If you post a comment in the forum, that content, the date,
+ time, and contents are collected and are visible to people who
+ have access to that particular forum.
+
+
+ -
+ If you are eligible for spayment we require payment information;
+ such as your contact information, payment account information,
+ and tax ID information as required by applicable laws. You may
+ be required to complete identity verification using a third
+ party that will require information that is country specific and
+ can include drivers license, passport, national identification
+ card or other government issued identification.
+
+
+ -
+ If you express interest in obtaining additional information
+ about our services; request customer support; use our “Contact
+ Us” or similar features; sign up for an event, webinar or
+ contest; or download certain content, we may require that you
+ provide to us contact information, such as your name, job title,
+ company name, address, phone number, email address or username
+ and password.
+
+
+ -
+ f you interact with our websites or emails, we automatically
+ collect information about your device and your usage of our
+ websites or emails (such as Internet Protocol (IP) addresses or
+ other identifiers, which may qualify as Personal Data (please
+ see the “What device and usage data we process” section, below)
+ using cookies, web beacons, or similar technologies;Automatic
+ Data Collection, such as IP address, MAC address or other device
+ identifiers.
+
+
+ -
+ if you use and interact with our services, we automatically
+ collect information about your device and your usage of our
+ services through log les and other technologies, some of which
+ may qualify as Personal Data (please see the “What device and
+ usage data we process” section, below);
+
+
+ -
+ If you voluntarily submit certain information to our services,
+ such as filling out a survey about your user experience, we
+ collect the information you have provided as part of that
+ request.
+
+
+
+ -
+ If you believe that your Personal Data has been provided to us
+ improperly, or want to exercise your rights relating to your
+ Personal Data, please contact us by using the information in the
+ “Contact Us” section below.
+
+
+
+ -
+ 2.2 PERSONAL DATA WE COLLECT FROM OTHER SOURCES
+
+
+
+ -
+ We also collect information about you from other sources including
+ third parties from whom we purchase Personal Data and from publicly
+ available information. We may combine this information with Personal
+ Data provided by you. This helps us update, expand, and analyze our
+ records, identify new customers, and create more tailored
+ advertising to provide services that may be of interest to you. The
+ Personal Data we collect from other sources includes identifiers,
+ professional or employment-related information, education
+ information, commercial information, visual information, internet
+ activity information, and inferences about preferences and
+ behaviors. In particular, we collect such Personal Data from the
+ following sources:
+
+ -
+
+
+ -
+ Third party providers of business contact information, including
+ mailing addresses, job titles, email addresses, phone numbers,
+ intent data (or user behavior data), IP addresses, social media
+ profiles, LinkedIn URLs and custom pro les, for purposes of
+ targeted advertising, delivering relevant email content, event
+ promotion and profiling, determining eligibility and verifying
+ contact information; and
+
+
+ -
+ Another individual at your organization who may provide us with
+ your business contact information for the purposes of obtaining
+ services; and
+
+
+ -
+ Platforms such as GitHub to manage code check-ins and pull
+ requests. If you participate in an open source or community
+ development project, we may associate your code repository
+ username with your community account so we can inform you of
+ program changes that are important to your participation or
+ relate to additional security requirements.
+
+
+
+
+
+
3. WHAT DEVICE AND USAGE DATA DO WE PROCESS?
+
+ -
+ We use common information-gathering tools, such as tools for
+ collecting usage data, cookies, web beacons, pixels, and similar
+ technologies to automatically collect information that may contain
+ Personal Data as you navigate our websites, our services, or
+ interact with emails we have sent to you.
+
+
+ -
+ 3.1 DEVICE AND USAGE DATA
+
+
+
+ -
+ As is true of most websites, we gather certain information
+ automatically when individual users visit our websites. This
+ information may include identifiers, commercial information, and
+ internet activity information such as IP address (or proxy server
+ information), device and application information, identification
+ numbers and features, location, browser type, plug-ins,
+ integrations, Internet service provider and/or mobile carrier, the
+ pages and les viewed, searches, referring website, app or ad,
+ operating system, system configuration information, advertising and
+ language preferences, date and time stamps associated with your
+ usage, and frequency of visits to the websites. This information is
+ used to analyze overall trends, help us provide and improve our
+ websites, offer a tailored experience for website users, and secure
+ and maintain our websites. In addition, we gather certain
+ information automatically as part of your use of our cloud products
+ and services. This information may include identifiers, commercial
+ information, and internet activity information such as IP address
+ (or proxy server), mobile device number, device and application
+ identification numbers, location, browser type, Internet service
+ provider or mobile carrier, the pages and profiles viewed, website
+ and webpage interactions including searches and other actions you
+ take, operating system and system configuration information and date
+ and time stamps associated with your usage. This information is used
+ to maintain the security of the services, to provide necessary
+ functionality, to improve performance of the services, to assess and
+ improve customer and user experience of the services, to review
+ compliance with applicable usage terms, to identify future
+ opportunities for development of the services, to assess capacity
+ requirements, to identify customer opportunities, and for the
+ security of Topcoder generally (in addition to the security of our
+ products and services). Some of the device and usage data collected
+ by the services, whether alone or in conjunction with other data,
+ could be personally identifying to you. Please note that this device
+ and usage data is primarily used to identify the uniqueness of each
+ user logging on (as opposed to specific individuals), apart from
+ where it is strictly required to identify an individual for security
+ purposes or as required as part of our provision of the services to
+ our customers.
+
+
+
+
+ 3.2 COOKIES, WEB BEACONS AND OTHER TRACKING TECHNOLOGIES ON OUR
+ WEBSITE AND IN EMAIL COMMUNICATIONS
+
+
+
+ -
+ We use technologies such as web beacons, pixels, tags, and
+ JavaScript, alone or in conjunction with cookies, to gather
+ information about the use of our websites and how people interact
+ with our emails. When you visit our websites, we, or an authorized
+ third party, may place a cookie on your device that collects
+ information, including Personal Data, about your online activities
+ over time and across different sites. Cookies allow us to track use,
+ infer browsing preferences, and improve and customize your browsing
+ experience. We use both session-based and persistent cookies on our
+ websites. Session-based cookies exist only during a single session
+ and disappear from your device when you close your browser or turn
+ off the device. Persistent cookies remain on your device after you
+ close your browser or turn your device off. To change your cookie
+ settings and preferences for one of our websites, click the Cookie
+ Preferences link in the footer of the page. You can also control the
+ use of cookies on your device, but choosing to disable cookies on
+ your device may limit your ability to use some features on our
+ websites and services. We also use web beacons and pixels on our
+ websites and in emails. For example, we may place a pixel in
+ marketing emails that notify us when you click on a link in the
+ email. We use these technologies to operate and improve our websites
+ and marketing emails. For instructions on how to unsubscribe from
+ our marketing emails, please see Section 9.4 below.
+
+
+ -
+ 3.3 SOCIAL MEDIA FEATURES
+
+
+ -
+ Our websites may use social media features, such as the Facebook
+ “like” button, the “Tweet” button and other sharing widgets (“Social
+ Media Features”). Social Media Features may allow you to post
+ information about your activities on our website to outside
+ platforms and social networks. Social Media Features may also allow
+ you to like or highlight information we have posted on our website
+ or our branded social media pages. Social Media Features are either
+ hosted by each respective platform or hosted directly on our
+ website. To the extent the Social Media Features are hosted by the
+ platforms themselves, and you click through to these from our
+ websites, the platform may receive information showing that you have
+ visited our websites. If you are logged in to your social media
+ account, it is possible that the respective social media network can
+ link your visit to our websites with your social media profile.
+ Topcoder also allows you to log in to certain of our websites using
+ sign-in services like Google Authentication. These services
+ authenticate your identity and provide you the option to share
+ certain Personal Data from these services with us such as your name
+ and email address to pre certain Personal Data from these services
+ with us such as your name and email address to prepopulate our
+ sign-up form. Your interactions with Social Media Features are
+ governed by the privacy policies of the companies providing them.
+
+
+
4. PURPOSES FOR WHICH WE PROCESS PERSONAL DATA AND THE LEGAL BASES
+ ON WHICH WE RELY
+
+
+ -
+ We collect and process your Personal Data for the following
+ purposes. Where required by law, we obtain your consent to use and
+ process your Personal Data for these purposes. Otherwise, we rely on
+ another authorized legal basis (including but not limited to the (a)
+ performance of a contract or (b) legitimate interest) to collect and
+ process your Personal Data.
+
+
+ -
+
+ -
+ Providing our websites and services: We process your Personal
+ Data to perform our contract with you for the use of our
+ websites and services and to fulfill our obligations under the
+ applicable terms of use and service; if we have not entered into
+ a contract with you, we base the processing of your Personal
+ Data on our legitimate interest to operate and administer our
+ websites and to provide you with content you access and request
+ (e.g., to download content from our websites);
+
+
+ -
+ Promoting the security of our websites and services: We process
+ your Personal Data by tracking use of our websites and services,
+ creating aggregated non- personal data, verifying accounts and
+ activity, investigating suspicious activity, and enforcing our
+ terms and policies to the extent it is necessary for our
+ legitimate interest in promoting the safety and security of the
+ services, systems and applications and in protecting our rights
+ and the rights of others;
+
+
+ -
+ Providing necessary functionality: We process your Personal Data
+ to perform our contract with you for the use of our websites and
+ services; if we have not entered into a contract with you, we
+ base the processing of your Personal Data on our legitimate
+ interest to provide you with the necessary functionality
+ required for your use of our websites and services;
+
+
+ -
+ Managing user registrations: If you have registered for an
+ account with us, we process your Personal Data by managing your
+ user account for the purpose of performing our contract with you
+ according to applicable terms of service;
+
+
+ -
+ Handling contact and user support requests: If you ll out a
+ “Submit a request” web form or request user support, or if you
+ contact us by other means including via a phone call, we process
+ your Personal Data to perform our contract with you and to the
+ extent it is necessary for our legitimate interest in fulfilling
+ your requests and communicating with you;
+
+
+ -
+ Managing event registrations and attendance: We process your
+ Personal Data to plan and host events or webinars for which you
+ have registered or that you attend, including sending related
+ communications to you, to perform our contract with you;
+
+
+ -
+ Managing contests or promotions: If you register for a contest
+ or promotion, we process your Personal Data to perform our
+ contract with you. Some contests or promotions have additional
+ rules containing information about how we will process your
+ Personal Data;
+
+
+ -
+ Managing payments: If you have provided financial information to
+ us, we process your Personal Data to verify that information and
+ to collect payments to the extent that doing so is necessary to
+ complete a transaction and perform our contract with you;
+
+
+ -
+ Developing and improving our websites and services: We process
+ your Personal Data to analyze trends and to track your usage of
+ and interactions with our websites and services to the extent it
+ is necessary for our legitimate interest in developing and
+ improving our websites and services and providing our users with
+ more relevant content and service offerings, or where we seek
+ your valid consent;
+
+
+ -
+ Assessing and improving user experience: We process device and
+ usage data as described in Section 4.1 above, which in some
+ cases may be associated with your Personal Data, to analyze
+ trends and assess and improve the overall user experience to the
+ extent it is necessary for our legitimate interest in developing
+ and improving the service offering, or where we seek your valid
+ consent;
+
+
+ -
+ Reviewing compliance with applicable usage terms: We process
+ your Personal Data to review compliance with the applicable
+ usage terms in our customer’s contract to the extent that it is
+ in our legitimate interest to ensure adherence to the relevant
+ terms;
+
+
+ -
+ Identifying customer opportunities: We process your Personal
+ Data to assess new potential customer opportunities to the
+ extent that it is in our legitimate interest to ensure that we
+ are meeting the demands of our customers and their users’
+ experiences;
+
+
+ -
+ Displaying personalized advertisements and content: We process
+ your Personal Data to conduct marketing research, advertise to
+ you, provide personalized information about us on and off our
+ websites and to provide other personalized content based upon
+ your activities and interests to the extent it is necessary for
+ our legitimate interest in advertising our websites or, where
+ necessary, to the extent you have provided your prior consent
+ (please see the “Your rights relating to your Personal Data”
+ section, below, to learn how you can control how the processing
+ of your Personal Data by Topcoder for personalized advertising
+ purposes);
+
+
+ -
+ Sending marketing communications: We will process your Personal
+ Data or device and usage data, which in some cases may be
+ associated with your Personal Data, to send you marketing
+ information, product recommendations and other non-transactional
+ communications (e.g., marketing newsletters, telemarketing
+ calls, SMS, or push notifications) about us and our affiliates
+ and partners, including information about our products,
+ promotions or events as necessary for our legitimate interest in
+ conducting direct marketing or to the extent you have provided
+ your prior consent (please see the “Your rights relating to your
+ Personal Data” section, below, to learn how you can control the
+ processing of your Personal Data by Topcoder for marketing
+ purposes); and
+
+
+ -
+ Complying with legal obligations: We process your Personal Data
+ when cooperating with public and government authorities, courts
+ or regulators in accordance with our legal obligations under
+ applicable laws to the extent this requires the processing or
+ disclosure of Personal Data to protect our rights or is
+ necessary for our legitimate interest in protecting against
+ misuse or abuse of our websites, protecting personal property or
+ safety, pursuing remedies available to us and limiting our
+ damages, complying with judicial proceedings, court orders or
+ legal processes, respond to lawful requests, or for auditing
+ purposes.
+
+
+ -
+ If we need to collect and process Personal Data by law, or under
+ a contract we have entered into with you, and you fail to
+ provide the required Personal Data when requested, we may not be
+ able to perform our contract with you.
+
+
+
+
+
5. WHO DO WE SHARE PERSONAL DATA WITH?
+
+ -
+ Topcoder does not sell Personal Data to marketers or unaffiliated
+ third parties. We may share your Personal Data as follows:
+
+
+ -
+
+ -
+ Service Providers: With our contracted service providers, who
+ provide services such as IT and system administration and
+ hosting, credit card processing, research and analytics,
+ marketing, customer support and data enrichment for the purposes
+ and pursuant to the legal bases described above;
+
+
+ -
+ Affiliates: If you use our websites to register for an event or
+ webinar organized by one of our affiliates, we may share your
+ Personal Data with the affiliate to the extent this is required
+ on the basis of the affiliate’s contract with you to process
+ your registration and ensure your participation in the event; in
+ such instances, our affiliate will process the relevant Personal
+ Data as a separate controller and will provide you with further
+ information on the processing of your Personal Data, where
+ required.
+
+
+ -
+ Event Sponsors: If you attend an event or webinar organized by
+ us, or download or access an asset on our website, we may share
+ your Personal Data with sponsors of the event. If required by
+ applicable law, you may consent to such sharing via the
+ registration form or by allowing your attendee badge to be
+ scanned at a sponsor booth. In these circumstances, your
+ information will be subject to the sponsors’ privacy statements.
+ If you do not wish for your information to be shared, you may
+ choose to not opt-in via event/webinar registration or elect to
+ not have your badge scanned, or you can opt-out in accordance
+ with Section 10 below;
+
+
+ -
+ Customers With Whom You Are Affiliated: If you use our services
+ as an authorized user, we may share your Personal Data with your
+ affiliated customer responsible for your access to the services
+ to the extent this is necessary for verifying accounts and
+ activity, investigating suspicious activity, or enforcing our
+ terms and policies;
+
+
+ -
+ Third party networks and websites: With third-party social media
+ networks, advertising networks and websites, so that Topcoder
+ can market and advertise on third party platforms and websites;
+
+
+ -
+ Professional Advisers: In individual instances, we may share
+ your Personal Data with professional advisers acting as service
+ providers, processors, or joint controllers – including lawyers,
+ bankers, auditors, and insurers who provide consultancy,
+ banking, legal, insurance and accounting services, and to the
+ extent we are legally obliged to share or have a legitimate
+ interest in sharing your Personal Data;
+
+
+ -
+ Third Parties Involved in a Corporate Transaction: If we are
+ involved in a merger, reorganization, dissolution or other
+ fundamental corporate change, or sell a website or business
+ unit, or if all or a portion of our business, assets or stock
+ are acquired by a third party, with such third party. In
+ accordance with applicable laws, we will use reasonable efforts
+ to notify you of any transfer of Personal Data to an
+ unaffiliated third party
+
+
+
+
+ -
+ We may also share anonymous or de-identified usage data with
+ Topcoders service providers for the purpose of helping Topcoder in
+ such analysis and improvements. Additionally, Topcoder may share
+ such anonymous or de-identified usage data on an aggregate basis in
+ the normal course of operating our business; for example, we may
+ share information publicly to show trends about the general use of
+ our services. Anyone using our communities, forums, blogs, or chat
+ rooms on our websites may read any Personal Data or other
+ information you choose to submit and post.
+
+
+
6. INTERNATIONAL TRANSFER OF PERSONAL DATA
+
+ -
+ We are a global business. Personal Data may be stored and processed
+ in any country where we have operations or where we engage service
+ providers as described above in section 5. By using our Services,
+ any personal information provided to Topcoder, where applicable law
+ permits, you consent to the transfer, processing, and storage of
+ such information outside of your country of residence where data
+ protection standards may be different. We may transfer Personal Data
+ that we maintain about you to recipients in countries other than the
+ country in which the Personal Data was originally collected,
+ including to the United States. Those countries may have data
+ protection rules that are different from those of your country.
+ However, we will take measures to ensure that any such transfers
+ comply with applicable data protection laws and that your Personal
+ Data remains protected to the standards described in this Privacy
+ Policy. In certain circumstances, courts, law enforcement agencies,
+ regulatory agencies or security authorities in those other countries
+ may be entitled to access your Personal Data.
+
+
+
7. CHILDREN
+
+ -
+ Topcoder recognizes the privacy interests of children and we
+ encourage parents and guardians to take an active role in their
+ children’s online activities and interests. Neither our Services or
+ website are intended for children under the age of 13. Topcoder does
+ not target its services or this Site to children under 13. Topcoder
+ does not knowingly collect personal information from children under
+ the age of 13.
+
+
+
8. HOW LONG DO WE KEEP YOUR PERSONAL DATA?
+
+ -
+ We store the information we collect for as long as it is necessary
+ for the purpose(s) for which we originally collected it. We may
+ retain certain information for legitimate business purposes or as
+ required by law.
+
+
+
9. YOUR CHOICES
+
+ -
+ As a Topcoder user you have choices about how to protect and limit
+ the collection, use, and disclosure of information about you.
+ Depending on your location and subject to applicable law, you may
+ have the following rights with regard to the Personal Data we
+ control about you:
+
+
+ -
+
+ - Access your Personal Data held by us
+
+ - Know more about how we processed your Personal Data;
+
+ -
+ Rectify inaccurate Personal Data and, taking into account the
+ purpose of processing the Personal Data, ensure it is complete;
+
+
+ -
+ Erase or delete your Personal Data (also referred to as the
+ right to be forgotten), to the extent permitted by applicable
+ data protection laws;
+
+
+ -
+ Restrict our processing of your Personal Data, to the extent
+ permitted by law; Transfer your
+
+
+ -
+ Personal Data to another controller, to the extent possible
+ (right to data portability)
+
+
+ -
+ Object to any processing of your Personal Data. Where we process
+ your Personal Data for direct marketing purposes or share it
+ with third parties for their own direct marketing purposes, you
+ can exercise your right to object at any time to such processing
+ without having to provide any specific reason for such
+ objection;
+
+
+ -
+ Opt out of certain disclosures of your Personal Data to third
+ parties;
+
+
+ -
+ If you’re under the age of 16, opt in to certain disclosures of
+ your Personal Data to third parties;
+
+
+ -
+ Not be discriminated against for exercising your rights
+ described above;
+
+
+ -
+ Not be subject to a decision based solely on automated
+ processing, including profiling, which produces legal effects
+ (“Automated Decision-Making”). Automated Decision-Making
+ currently does not take place on our websites or in our
+ services; and
+
+
+ -
+ Withdraw your consent at any time (to the extent we base
+ processing on consent), without affecting the lawfulness of the
+ processing based on such consent before its withdrawal.
+
+
+
+
+ -
+ 9.1 ACCESSING AND CHANGING YOUR INFORMATION
+
+
+ -
+ You can access and change certain information through the Services.
+ See our Help Center page for more information. You can also request
+ a copy of the personal information Topcoder maintains about you by
+ following the process described here.
+
+
+ -
+ 9.2 DELETING YOUR ACCOUNT
+
+
+ -
+ You may delete your account information at any time from the user
+ preferences page. When you delete your account, your profile is no
+ longer visible to other users. Please note, however, that the
+ contests, tasks, and messages you submitted prior to deleting your
+ account will still be visible toSupport others. We may also retain
+ certain information about you as required by law or for legitimate
+ business purposes after you delete your account.
+
+
+ -
+ 9.3 CONTROLLING THE USE OF COOKIES
+
+
+ -
+ To change your cookie settings and preferences for one of our
+ websites, click the Cookie Preferences link in the footer of the
+ page. You can also control the use of cookies on your device, but
+ choosing to disable cookies on your device may limit your ability to
+ use some features on our websites and services.
+
+
+ -
+ 9.4 CONTROLLING ADVERTISING AND ANALYTICS
+
+
+
+ -
+ Some analytics providers we partner with may provide specific
+ opt-out mechanisms and we may provide, as needed and as available,
+ additional tools and third-party services that allow you to better
+ understand cookies and how you can opt-out. For example, you may
+ manage the use and collection of certain information by Google
+ Analytics .
+
+
+ -
+ You may also generally opt out of receiving personalized
+ advertisements from certain third-party advertisers and ad networks.
+ To learn more about these advertisements or to opt out, please visit
+ the websites of the Digital Advertising Alliance and the Network
+ Advertising Initiative, or if you are a user in the European
+ Economic Area, Your Online Choices.
+
+
+ -
+ You may also generally opt out of receiving personalized
+ advertisements from certain third-party advertisers and ad networks.
+ To learn more about these advertisements or to opt out, please visit
+ the websites of the Digital Advertising Alliance and the Network
+ Advertising Initiative
+
+
+ -
+ 9.5 DO NOT TRACK
+
+
+
+ -
+ Most modern web browsers give you the option to send a Do Not Track
+ signal to the websites you visit, indicating that you do not wish to
+ be tracked. However, there is no accepted standard for how a website
+ should respond to this signal, and we do not take any action in
+ response to this signal. Instead, in addition to publicly available
+ third-party tools, we offer you the choices described in this policy
+ to manage the collection and use of information about you.
+
+
+ -
+ 9.6 CONTROLLING PROMOTIONAL COMMUNICATIONS
+
+
+
+ -
+ You may opt out of receiving some or all categories of promotional
+ communications from us by following the instructions in those
+ communications or by updating your email options in your account
+ preferences here. If you opt out of promotional communications, we
+ may still send you non-promotional communications, such as
+ information about your account or your use of the Services.
+
+
+
10. HOW WE SECURE YOUR PERSONAL DATA
+
+ -
+ We take appropriate precautions including organizational, technical,
+ and physical measures to help safeguard against accidental or
+ unlawful destruction, loss, alteration, and unauthorized disclosure
+ of, or access to, the Personal Data we process or use. While we
+ follow generally accepted standards to protect Personal Data, no
+ method of storage or transmission is 100% secure You are solely
+ responsible for protecting your password limiting transmission is
+ 100% secure. You are solely responsible for protecting your
+ password, limiting access to your devices and signing out of
+ websites after your sessions. If you have any questions about the
+ security of our websites, please contact us by using the information
+ in the “Contact us” section, below.
+
+
+
11. CHANGES TO THIS POLICY
+
+ -
+ We may change this Privacy Policy from time to time to reflect new
+ services, changes in our Personal Data practices or relevant laws.
+ The “Last updated” legend at the top of this Privacy Policy
+ indicates when this Privacy Policy was last revised. Any changes are
+ effective when we post the revised Privacy Policy on the Services.
+ We may provide you with disclosures and alerts regarding the Privacy
+ Policy or Personal Data collected by posting them on our website
+ and, if you are a User, by contacting you through our services.
+
+
+
12. HOW TO CONTACT US
+
+ -
+ To exercise your rights regarding your Personal Data, or if you have
+ questions regarding this Privacy Statement or our privacy practices
+ please fill out
+
+ this form
+
+ , email us at
+ privacy@topcoder.com, call
+ us at 855 867-1356, or write to us at:
+
+
+ -
+ Attention: Topcoder Legal 201 South Capitol Avenue, Ste. 1100
+ Indianapolis, IN 46225
+
+
+
13. CCPA RIGHTS AND CHOICES
+
+ -
+ Section 13 of this Privacy Policy applies solely to visitors, users,
+ and others who reside in the State of California (“consumers” or
+ “you”) who access the Services owned and operated by Topcoder. We
+ adopt this notice to comply with the California Consumer Privacy Act
+ of 2018 (“CCPA”) and other California privacy laws. Any terms
+ defined in the CCPA have the same meaning when used in this notice.
+ The California Consumer Privacy Act requires businesses to disclose
+ whether they sell Personal Data. We do not sell Personal Data. We
+ may share Personal Data with third parties or allow them to collect
+ Personal Data from our sites or Services if those third parties are
+ authorized service providers or business partners who have agreed to
+ our contractual limitations as to their retention, use, and
+ disclosure of such Personal Data, or if you use Topcoder sites or
+ Services to interact with third parties or direct us to disclose
+ your Personal Data to third parties.
+
+
+ -
+ We collect information that identifies, relates to, describes,
+ references, is capable of being associated with, or could reasonably
+ be linked, directly or indirectly, with a particular consumer or
+ device (“personal information”). In particular, we have collected,
+ and have disclosed for a business purpose, the following categories
+ of personal information from consumers in the last twelve (12)
+ months:
+
+
+ - ----
+ -
+ We obtain the above categories of information from the sources more
+ particularly described in Section 2 of this Privacy Policy.
+
+
+ -
+ We may disclose your personal information for a business purpose to
+ the categories of third parties described in Section 5 of this
+ Privacy Policy. When we disclose personal information for a business
+ purpose, we enter a contract that describes the purpose and requires
+ the recipient to both keep that personal information confidential
+ and not use it for any purpose except performing the contract.
+
+
+ -
+ We may use or disclose the personal information we collect for one
+ or more of the business purposes more particularly described in
+ Sections 3, 4 and 5 of this Privacy Policy
+
+
+ -
+ California law grants state residents certain rights, including the
+ rights to access specific types of Personal Data, to learn how we
+ process Personal Data, and to request deletion of Personal Data.. We
+ will not deny your use of our services, or provide you a different
+ level or quality of services for exercising any of your rights unde
+
+ -
+
+ -
+ 1. Please note that your request does not ensure complete or
+ comprehensive removal of the content or information, because,
+ for example, some of your content may have been reposted by
+ another user. Please also note that you may not be entitled to
+ deletion of your personal information, where such retention is
+ necessary for Topcoder to: Complete the transaction for which
+ the personal information was collected, fulfill the terms of a
+ written warranty or product recall conducted in accordance
+ with federal law, provide a good or service requested by you,
+ or reasonably anticipated within the context of our ongoing
+ business relationship with you, or otherwise perform a
+ contract between us.
+
+
+ -
+ 2. Detect security incidents, protect against malicious,
+ deceptive, fraudulent, or illegal activity; or prosecute those
+ responsible for that activity.
+
+
+ -
+ 3. Debug to identify and repair errors that impair existing
+ intended functionality.
+
+
+ -
+ 4. Exercise free speech, ensure the right of another consumer
+ to exercise that consumer’s right of free speech, or exercise
+ another right provided for by law.
+
+
+ -
+ 5. Comply with the California Electronic Communications
+ Privacy Act pursuant to Chapter 3.6 (commencing with Section
+ 1546) of Title 12 of Part 2 of the Penal Code.
+
+
+ -
+ 6. Engage in public or peer-reviewed scientific, historical,
+ or statistical research in the public interest that adheres to
+ all other applicable ethics and privacy laws, when our
+ deletion of the information is likely to render impossible or
+ seriously impair the achievement of such research, if you have
+ provided informed consent for the same.
+
+
+ -
+ 7. To enable solely internal uses that are reasonably aligned
+ with your expectations, based on your relationship with
+ Topcoder.
+
+
+ -
+ 9. Otherwise use your personal information, internally, in a
+ lawful manner that is compatible with the context in which you
+ provided the information. Support For information on how to
+ exercise your rights, please refer to Section 9 of this
+ Privacy Statement. If you are an authorized agent wishing to
+ exercise rights on behalf of a California resident, please
+ contact us using the information in the “How to Contact Us”
+ section above and provide us with a copy of the consumer’s
+ written authorization designating you as their agent.
+
+
+ -
+ We may need to verify your identity and place of residence
+ before completing your rights request.
+
+
+ -
+ Effective Date May 31, 2017 – Updated Jan 27, 2022
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default PrivacyPolicyModal
diff --git a/src-ts/lib/modals/privacy-policy-modal/index.ts b/src-ts/lib/modals/privacy-policy-modal/index.ts
new file mode 100644
index 000000000..364be4a28
--- /dev/null
+++ b/src-ts/lib/modals/privacy-policy-modal/index.ts
@@ -0,0 +1 @@
+export { default as PrivacyPolicyModal } from './PrivacyPolicyModal'
diff --git a/src-ts/lib/modals/terms-modal/TermsModal.module.scss b/src-ts/lib/modals/terms-modal/TermsModal.module.scss
new file mode 100644
index 000000000..1b2ffb43b
--- /dev/null
+++ b/src-ts/lib/modals/terms-modal/TermsModal.module.scss
@@ -0,0 +1,37 @@
+@import "../../styles/includes";
+
+.container {
+ line-height: 20px;
+
+ ol {
+ padding: 0;
+ list-style: none;
+ }
+
+ h4 {
+ margin-top: 20px;
+ }
+
+ p {
+ margin-bottom: 20px;
+
+ &.sm {
+ font-size: 14px;
+ }
+ }
+
+ ul.a {
+ list-style-type: circle;
+ list-style: circle;
+
+ li {
+ padding-left: 0;
+ margin-left: 20px;
+ }
+ }
+
+ a {
+ text-decoration: underline;
+ color: $link-blue-light;
+ }
+}
diff --git a/src-ts/lib/modals/terms-modal/TermsModal.tsx b/src-ts/lib/modals/terms-modal/TermsModal.tsx
new file mode 100644
index 000000000..69083ab1d
--- /dev/null
+++ b/src-ts/lib/modals/terms-modal/TermsModal.tsx
@@ -0,0 +1,212 @@
+import { FC } from 'react'
+
+import { BaseModal } from '../base-modal'
+
+import styles from './TermsModal.module.scss'
+
+export interface TermsModal {
+ isOpen: boolean
+ onClose: () => void
+}
+
+const TermsModal: FC = ({ isOpen, onClose }: TermsModal) => (
+
+
+
+ -
+
- Date of Last Revision: Jan 27, 2022
+
+ -
+ This User Agreement (the "Agreement") is a contract
+ between you (referred to herein as "User") and Topcoder LLC.
+ ("Topcoder") and applies to User's use and viewing of
+
+ {' '}
+ topcoder.com
+
+ , related sub-domains (“Topcoder.com”). By visiting Topcoder.com, User
+ has accepted this Agreement and has agreed to be bound by the terms of
+ this Agreement. Topcoder may amend this Agreement at any time by
+ posting a revised version on Topcoder.com. The revised version will be
+ effective at the time Topcoder posts it.
+
+
+ -
+
+ -
+
1.ACCOUNT
+
+ -
+ User may set up an account in connection with the use of
+ Topcoder.com. User may not use a third party's account without
+ permission. When setting up an account, User must supply accurate
+ and complete information. User is solely responsible for its
+ account and everything that happens on its account. User shall
+ protect its account log-in information and User shall report any
+ unauthorized use of its account to Topcoder immediately. User may
+ not transfer its account to any third party. Topcoder is not
+ liable for any damages or losses caused by someone using User's
+ account without User's permission.
+
+ -
+
2.PRIVACY
+
+ -
+ User agrees to comply with all terms of the Topcoder Privacy
+ Policy available at
+
+ {' '}
+ https://www.topcoder.com/policy/privacy-policy/
+
+
+ -
+
3.GENERAL USE OF TOPCODER.COM.
+
+ -
+ Except as expressly authorized hereunder, Topcoder.com and content
+ therein may not be reproduced, duplicated, copied, sold, resold,
+ visited, reverse- engineered or otherwise exploited for any
+ commercial purpose without Topcoder's prior written authorization.
+ Topcoder reserves the right to alter or discontinue Topcoder.com,
+ in whole or in part, at any time in Topcoder's sole discretion.
+ Topcoder.com is not intended for use by children under the age of
+ 13. Parental permission is required to use this Website if the
+ User has not reached the age of majority in the User's
+ jurisdiction of primary residence and citizenship. The User must
+ be at least 18 years old and have reached the age of majority in
+ such User's jurisdiction of primary residence and citizenship to
+ make any purchases from Topcoder.com. Subject to and conditioned
+ upon User's compliance with this Agreement, Topcoder grants User a
+ non-exclusive, non- transferable, limited right and license,
+ without right of sublicense, to access and use Topcoder.com,
+ including any images, text, graphics, sounds, data, links and
+ other materials incorporated therein solely as made available by
+ Topcoder and solely for User's own personal purposes. Except as
+ expressly authorized by this Agreement, User may not use,
+ reproduce, distribute, modify, transmit or publicly display any
+ portion of Topcoder.com or create derivative works of any portion
+ Topcoder.com without Topcoder's written consent. While using
+ Topcoder.com, User agrees not to:
+
-
+
+
+ -
+ Impersonate any person or entity or use any fraudulent,
+ misleading or inaccurate email address or other contact
+ information;
+
+ -
+ Restrict or inhibit any other user from using Topcoder.com,
+ including, without limitation, by means of "hacking" or
+ defacing any portion of Topcoder.com;
+
+ - Violate any applicable laws or regulations;
+ -
+ Upload to, transmit through, or display on Topcoder.com or
+ via communications related to usage of the Site (a) any
+ material that is unlawful, fraudulent, threatening, abusive,
+ libelous, defamatory, obscene or otherwise objectionable, or
+ infringes our or any third party's intellectual property or
+ other rights; (b) any confidential, proprietary or trade
+ secret information of any third party; or (c) any
+ advertisements, solicitations, chain letters, pyramid
+ schemes, investment opportunities or other unsolicited
+ commercial communication (except as otherwise expressly
+ permitted by us);
+
+ - Engage in spamming;
+ -
+ Transmit any software or other materials that contain any
+ viruses, worms, trojan horses, defects, or other destructive
+ items;
+
+ -
+ Modify, adapt, translate, distribute, reverse engineer,
+ decompile or disassemble any portion of our Sites and Apps;
+ and
+
+ -
+ Remove any copyright, trademark or other proprietary rights
+ notices contained in or displayed on any portion of
+ Topcoder.com
+
+ -
+ If User fails to comply with the above rules, such failure
+ will constitute a violation of this Agreement, and, in
+ addition to any other rights or remedies Topcoder may have,
+ Topcoder may immediately terminate User's access to and use
+ of Topcoder.com and related services.
+
+
+
+ -
+
4.GENERAL.
+
+ -
+ Each party is an independent contractor of the other and neither
+ is an employee, agent, partner or joint venturer of the other.
+ Neither party shall make any commitment, by contract or
+ otherwise, binding upon the other or represent that it has any
+ authority to do so. This Agreement will bind and inure to the
+ benefit of each party's permitted successors and assigns.
+ Neither party shall assign this Agreement without the advance
+ written consent of the other party, except that Topcoder may
+ assign this Agreement to an affiliate or in connection with a
+ merger, reorganization, acquisition or other transfer of all or
+ part of Topcoder's assets or voting securities. Any notice,
+ report, approval or consent required or permitted under this
+ Agreement will be sent to Topcoder at c/o Appirio Inc., 201
+ South Capitol Avenue Ste. 1100, Indianapolis, Indiana 46225,
+ Attention: General Counsel, Email: gc@appirio.com or to User at
+ the contact information provided by User through Topcoder.com or
+ in an order form. Any waiver by either party of any breach of
+ this Agreement, whether express or implied, will not constitute
+ a waiver of any other or subsequent breach. No provision of the
+ Agreement will be waived by any act, omission or knowledge of a
+ party or its agents or employees except by an instrument in
+ writing expressly waiving such provision and signed by a duly
+ authorized officer of the waiving party. If any provision of
+ this Agreement is adjudged by any court of competent
+ jurisdiction to be unenforceable or invalid, that provision
+ shall be limited or eliminated to the minimum extent necessary
+ so that this Agreement will otherwise remain in full force and
+ effect. Neither party shall be liable to the other for any delay
+ of failure to perform any obligation under this Agreement
+ (except for a failure to pay fees) if the delay or failure is
+ due to events which are beyond the reasonable control of such
+ party, including but not limited to any strike, blockade, war,
+ act of terrorism, riot, natural disaster, failure or
+ diminishment of power or of telecommunications or data networks
+ or services, or refusal of approval or a license by a government
+ agency. This Agreement will be deemed to have been made in, and
+ shall be construed pursuant to the laws of the State of Delaware
+ without regard to its conflicts of laws, provisions, or the
+ United Nations Convention on International Sale of Goods. The
+ jurisdiction and venue for actions related to this Agreement
+ shall be the state and federal courts located in Wilmington
+ County, Delaware and both parties hereby submit to the personal
+ jurisdiction of such courts. Both parties hereby waive their
+ right to a trial by jury. Any waivers or amendments shall be
+ effective only if made in writing signed by a representative of
+ the respective parties authorized to bind the parties. No
+ provision of any purchase order or other business form
+ (including but not limited to security access forms of any kind)
+ employed by either party will supersede the terms and conditions
+ of this Agreement, and any such document shall be for
+ administrative purposes
+
+
+
+
+
+
+
+
+)
+
+export default TermsModal
diff --git a/src-ts/lib/modals/terms-modal/index.ts b/src-ts/lib/modals/terms-modal/index.ts
new file mode 100644
index 000000000..621498cdf
--- /dev/null
+++ b/src-ts/lib/modals/terms-modal/index.ts
@@ -0,0 +1 @@
+export { default as TermsModal } from './TermsModal'
diff --git a/src-ts/lib/page-footer/PageFooter.module.scss b/src-ts/lib/page-footer/PageFooter.module.scss
new file mode 100644
index 000000000..7165e885f
--- /dev/null
+++ b/src-ts/lib/page-footer/PageFooter.module.scss
@@ -0,0 +1,61 @@
+@import '../styles/includes';
+@import '../styles/typography';
+
+.footer-wrap {
+ height: $footer-height;
+ border-top: $border-xs solid $black-10;
+}
+
+.footer-inner {
+ display: flex;
+
+ align-items: center;
+ padding: $pad-lg 0;
+ max-width: $xxl-min;
+ margin: 0 auto;
+
+ @include pagePaddings;
+
+ @extend .body-ultra-small;
+ color: $black-100;
+
+ @include ltesm {
+ flex-direction: column;
+ gap: $pad-sm;
+ }
+}
+
+.utils {
+ display: flex;
+ align-items: center;
+ gap: $pad-lg;
+
+ > div > * + * {
+ margin-left: $pad-lg;
+ }
+
+ @include ltesm {
+ flex-direction: column;
+ gap: $pad-sm;
+
+ > div > * + * {
+ margin-left: $pad-sm;
+ }
+ }
+}
+
+.social {
+ display: flex;
+ align-items: center;
+ gap: $pad-xs;
+ color: $black-60;
+
+ margin-left: auto;
+
+ a {
+ display: flex;
+ }
+ @include ltesm {
+ margin-right: auto;
+ }
+}
diff --git a/src-ts/lib/page-footer/PageFooter.tsx b/src-ts/lib/page-footer/PageFooter.tsx
new file mode 100644
index 000000000..c90c5d4f4
--- /dev/null
+++ b/src-ts/lib/page-footer/PageFooter.tsx
@@ -0,0 +1,84 @@
+import { Dispatch, FC, MouseEvent, SetStateAction, useState } from 'react'
+
+import { ContactSupportModal, OrderContractModal, PrivacyPolicyModal, TermsModal } from '../modals'
+import { ProfileProvider } from '../profile-provider'
+import { Facebook, Instagram, LinkedIn, Twitter, Youtube } from '../social-links'
+
+import styles from './PageFooter.module.scss'
+
+const PageFooter: FC<{}> = () => {
+
+ const [isContactSupportModalOpen, setIsContactSupportModalOpen]: [boolean, Dispatch>] = useState(false)
+ const [isOrderContractModalOpen, setIsOrderContractModalOpen]: [boolean, Dispatch>] = useState(false)
+ const [isPrivacyModalOpen, setIsPrivacyModalOpen]: [boolean, Dispatch>] = useState(false)
+ const [isTermsModalOpen, setIsTermsModalOpen]: [boolean, Dispatch>] = useState(false)
+
+ function handleClick(event: MouseEvent, setter: Dispatch>): void {
+ event.preventDefault()
+ setter(true)
+ }
+
+ return (
+
+
+
+ setIsContactSupportModalOpen(false)}
+ />
+
+
+
setIsOrderContractModalOpen(false)}
+ />
+
+ setIsPrivacyModalOpen(false)}
+ />
+
+ setIsTermsModalOpen(false)}
+ />
+
+
+
+ )
+}
+
+export default PageFooter
diff --git a/src-ts/lib/page-footer/index.ts b/src-ts/lib/page-footer/index.ts
new file mode 100644
index 000000000..c7434045b
--- /dev/null
+++ b/src-ts/lib/page-footer/index.ts
@@ -0,0 +1 @@
+export { default as PageFooter } from './PageFooter'
diff --git a/src-ts/lib/pagination/index.ts b/src-ts/lib/pagination/index.ts
new file mode 100644
index 000000000..3b90b0623
--- /dev/null
+++ b/src-ts/lib/pagination/index.ts
@@ -0,0 +1,2 @@
+export * from './page.model'
+export * from './sort.model'
diff --git a/src-ts/lib/pagination/page.model.ts b/src-ts/lib/pagination/page.model.ts
new file mode 100644
index 000000000..385c69d9c
--- /dev/null
+++ b/src-ts/lib/pagination/page.model.ts
@@ -0,0 +1,7 @@
+import { Sort } from './sort.model'
+
+export interface Page {
+ number: number // this is a page number, not a page index; therefore, it starts at 1
+ size: number
+ sort: Sort
+}
diff --git a/src-ts/lib/pagination/sort.model.ts b/src-ts/lib/pagination/sort.model.ts
new file mode 100644
index 000000000..ab0dc613f
--- /dev/null
+++ b/src-ts/lib/pagination/sort.model.ts
@@ -0,0 +1,4 @@
+export interface Sort {
+ direction: 'asc' | 'desc'
+ fieldName: string
+}
diff --git a/src-ts/lib/portal/Portal.tsx b/src-ts/lib/portal/Portal.tsx
new file mode 100755
index 000000000..d804eee36
--- /dev/null
+++ b/src-ts/lib/portal/Portal.tsx
@@ -0,0 +1,53 @@
+import { FC, MutableRefObject, ReactNode, useEffect, useMemo } from 'react'
+import { createPortal } from 'react-dom'
+
+interface PortalProps {
+ children: ReactNode
+ className?: string,
+ portalId?: string
+ portalNode?: HTMLElement
+ portalRef?: MutableRefObject,
+}
+
+const Portal: FC = (
+{
+ portalId,
+ portalNode,
+ children,
+ className,
+ portalRef,
+}: PortalProps) => {
+
+ const defaultPortalNode: HTMLElement = useMemo(() => {
+ if (portalNode) {
+ return
+ }
+
+ if (portalId) {
+ return document.getElementById(portalId)
+ }
+
+ const backupHtmlNode: HTMLElement = document.createElement('div')
+ if (className) {
+ backupHtmlNode.classList.add(className)
+ }
+ document.body.appendChild(backupHtmlNode)
+ return backupHtmlNode
+ }, [portalNode, className]) as HTMLElement
+
+ useEffect(() => {
+ return () => {
+ if (defaultPortalNode) {
+ document.body.removeChild(defaultPortalNode)
+ }
+ }
+ }, [])
+
+ if (portalRef) {
+ portalRef.current = portalNode ?? defaultPortalNode
+ }
+
+ return createPortal(children, portalNode ?? defaultPortalNode)
+}
+
+export default Portal
diff --git a/src-ts/lib/portal/index.ts b/src-ts/lib/portal/index.ts
new file mode 100755
index 000000000..894f64b00
--- /dev/null
+++ b/src-ts/lib/portal/index.ts
@@ -0,0 +1 @@
+export { default as Portal } from './Portal'
diff --git a/src/lib/profile-provider/password-update-request.model.ts b/src-ts/lib/profile-provider/change-password-request.model.ts
similarity index 53%
rename from src/lib/profile-provider/password-update-request.model.ts
rename to src-ts/lib/profile-provider/change-password-request.model.ts
index fa537e145..eb012599a 100644
--- a/src/lib/profile-provider/password-update-request.model.ts
+++ b/src-ts/lib/profile-provider/change-password-request.model.ts
@@ -1,4 +1,4 @@
-export interface PasswordUpdateRequest {
+export interface ChangePasswordRequest {
newPassword: string
password: string
}
diff --git a/src-ts/lib/profile-provider/edit-name-request.model.ts b/src-ts/lib/profile-provider/edit-name-request.model.ts
new file mode 100644
index 000000000..fe631a167
--- /dev/null
+++ b/src-ts/lib/profile-provider/edit-name-request.model.ts
@@ -0,0 +1,4 @@
+export interface EditNameRequest {
+ firstName: string
+ lastName: string
+}
diff --git a/src/lib/profile-provider/index.ts b/src-ts/lib/profile-provider/index.ts
similarity index 52%
rename from src/lib/profile-provider/index.ts
rename to src-ts/lib/profile-provider/index.ts
index 69a4938f8..2e8837381 100644
--- a/src/lib/profile-provider/index.ts
+++ b/src-ts/lib/profile-provider/index.ts
@@ -1,6 +1,6 @@
-export * from './password-update-request.model'
+export * from './change-password-request.model'
+export * from './edit-name-request.model'
export * from './profile-context-data.model'
export { default as profileContext, defaultProfileContextData } from './profile.context'
-export { ProfileProvider } from './profile.provider'
+export * from './profile.provider'
export * from './user-profile.model'
-export * from './user-profile-update-request.model'
diff --git a/src/lib/profile-provider/profile-context-data.model.ts b/src-ts/lib/profile-provider/profile-context-data.model.ts
similarity index 62%
rename from src/lib/profile-provider/profile-context-data.model.ts
rename to src-ts/lib/profile-provider/profile-context-data.model.ts
index a9f93acaa..2cd45ee18 100644
--- a/src/lib/profile-provider/profile-context-data.model.ts
+++ b/src-ts/lib/profile-provider/profile-context-data.model.ts
@@ -1,9 +1,10 @@
-import { PasswordUpdateRequest } from './password-update-request.model'
+import { ChangePasswordRequest } from './change-password-request.model'
import { UserProfile } from './user-profile.model'
export interface ProfileContextData {
+ changePassword: (userId: number, request: ChangePasswordRequest) => Promise
initialized: boolean
+ isLoggedIn: boolean
profile?: UserProfile
- updatePassword: (userId: number, request: PasswordUpdateRequest) => Promise
updateProfile: (updatedProfileContext: ProfileContextData) => Promise
}
diff --git a/src/lib/profile-provider/profile-functions/index.ts b/src-ts/lib/profile-provider/profile-functions/index.ts
similarity index 62%
rename from src/lib/profile-provider/profile-functions/index.ts
rename to src-ts/lib/profile-provider/profile-functions/index.ts
index b541316cd..fe44706e7 100644
--- a/src/lib/profile-provider/profile-functions/index.ts
+++ b/src-ts/lib/profile-provider/profile-functions/index.ts
@@ -1,4 +1,4 @@
export {
getAsync as profileGetAsync,
- updateAsync as profileUpdateAsync,
+ editNameAsync as profileEditNameAsync,
} from './profile.functions'
diff --git a/src/lib/profile-provider/profile-functions/profile-store/index.ts b/src-ts/lib/profile-provider/profile-functions/profile-store/index.ts
similarity index 62%
rename from src/lib/profile-provider/profile-functions/profile-store/index.ts
rename to src-ts/lib/profile-provider/profile-functions/profile-store/index.ts
index c5218a90c..1fd1267c7 100644
--- a/src/lib/profile-provider/profile-functions/profile-store/index.ts
+++ b/src-ts/lib/profile-provider/profile-functions/profile-store/index.ts
@@ -1,4 +1,4 @@
export {
get as profileStoreGet,
- put as profileStorePut,
+ patchName as profileStorePatchName,
} from './profile-xhr.store'
diff --git a/src/lib/profile-provider/profile-functions/profile-store/profile-endpoint.config.ts b/src-ts/lib/profile-provider/profile-functions/profile-store/profile-endpoint.config.ts
similarity index 100%
rename from src/lib/profile-provider/profile-functions/profile-store/profile-endpoint.config.ts
rename to src-ts/lib/profile-provider/profile-functions/profile-store/profile-endpoint.config.ts
diff --git a/src-ts/lib/profile-provider/profile-functions/profile-store/profile-xhr.store.ts b/src-ts/lib/profile-provider/profile-functions/profile-store/profile-xhr.store.ts
new file mode 100644
index 000000000..cfd458995
--- /dev/null
+++ b/src-ts/lib/profile-provider/profile-functions/profile-store/profile-xhr.store.ts
@@ -0,0 +1,15 @@
+import { xhrGetAsync, xhrPutAsync } from '../../../functions/xhr-functions'
+import { EditNameRequest } from '../../edit-name-request.model'
+import { UserProfile } from '../../user-profile.model'
+
+import { profile as profileUrl } from './profile-endpoint.config'
+
+export function get(handle: string): Promise {
+ return xhrGetAsync(profileUrl(handle))
+}
+
+// NOTE: this method is named patch bc the request body is just a partial profile,
+// but the underlying xhr request is actually a put b/c the api doesn't support patch
+export function patchName(handle: string, request: EditNameRequest): Promise {
+ return xhrPutAsync(profileUrl(handle), request)
+}
diff --git a/src/lib/profile-provider/profile-functions/profile.functions.ts b/src-ts/lib/profile-provider/profile-functions/profile.functions.ts
similarity index 52%
rename from src/lib/profile-provider/profile-functions/profile.functions.ts
rename to src-ts/lib/profile-provider/profile-functions/profile.functions.ts
index d255555d4..b6056a4d1 100644
--- a/src/lib/profile-provider/profile-functions/profile.functions.ts
+++ b/src-ts/lib/profile-provider/profile-functions/profile.functions.ts
@@ -1,14 +1,14 @@
import { tokenGetAsync } from '../../functions/token-functions'
-import { UserProfileUpdateRequest } from '../user-profile-update-request.model'
+import { EditNameRequest } from '../edit-name-request.model'
import { UserProfile } from '../user-profile.model'
-import { profileStoreGet, profileStorePut } from './profile-store'
+import { profileStoreGet, profileStorePatchName } from './profile-store'
export async function getAsync(handle?: string): Promise {
handle = handle || (await tokenGetAsync())?.handle
return !handle ? Promise.resolve(undefined) : profileStoreGet(handle)
}
-export async function updateAsync(handle: string, profile: UserProfileUpdateRequest): Promise {
- return profileStorePut(handle, profile)
+export async function editNameAsync(handle: string, profile: EditNameRequest): Promise {
+ return profileStorePatchName(handle, profile)
}
diff --git a/src/lib/profile-provider/profile.context.tsx b/src-ts/lib/profile-provider/profile.context.tsx
similarity index 84%
rename from src/lib/profile-provider/profile.context.tsx
rename to src-ts/lib/profile-provider/profile.context.tsx
index b46ea8180..d61f6c3c2 100644
--- a/src/lib/profile-provider/profile.context.tsx
+++ b/src-ts/lib/profile-provider/profile.context.tsx
@@ -3,8 +3,9 @@ import { Context, createContext } from 'react'
import { ProfileContextData } from './profile-context-data.model'
export const defaultProfileContextData: ProfileContextData = {
+ changePassword: () => Promise.resolve(),
initialized: false,
- updatePassword: () => Promise.resolve(),
+ isLoggedIn: false,
updateProfile: () => Promise.resolve(undefined),
}
diff --git a/src/lib/profile-provider/profile.provider.tsx b/src-ts/lib/profile-provider/profile.provider.tsx
similarity index 79%
rename from src/lib/profile-provider/profile.provider.tsx
rename to src-ts/lib/profile-provider/profile.provider.tsx
index daf70ccc9..60e32e24c 100644
--- a/src/lib/profile-provider/profile.provider.tsx
+++ b/src-ts/lib/profile-provider/profile.provider.tsx
@@ -2,11 +2,11 @@ import { Dispatch, FC, ReactNode, SetStateAction, useEffect, useState } from 're
import { userUpdatePasswordAsync } from '../functions'
-import { PasswordUpdateRequest } from './password-update-request.model'
+import { ChangePasswordRequest } from './change-password-request.model'
+import { EditNameRequest } from './edit-name-request.model'
import { ProfileContextData } from './profile-context-data.model'
-import { profileGetAsync, profileUpdateAsync } from './profile-functions'
+import { profileEditNameAsync, profileGetAsync } from './profile-functions'
import { default as profileContext, defaultProfileContextData } from './profile.context'
-import { UserProfileUpdateRequest } from './user-profile-update-request.model'
import { UserProfile } from './user-profile.model'
export const ProfileProvider: FC<{ children: ReactNode }> = ({ children }: { children: ReactNode }) => {
@@ -14,7 +14,7 @@ export const ProfileProvider: FC<{ children: ReactNode }> = ({ children }: { chi
const [profileContextData, setProfileContextData]: [ProfileContextData, Dispatch>]
= useState(defaultProfileContextData)
- function updatePassword(userId: number, request: PasswordUpdateRequest): Promise {
+ function changePassword(userId: number, request: ChangePasswordRequest): Promise {
return userUpdatePasswordAsync(userId, request.password, request.newPassword)
}
@@ -26,13 +26,12 @@ export const ProfileProvider: FC<{ children: ReactNode }> = ({ children }: { chi
throw new Error('Cannot update an undefined profile')
}
- const request: UserProfileUpdateRequest = {
- email: profile.email,
+ const request: EditNameRequest = {
firstName: profile.firstName,
lastName: profile.lastName,
}
- return profileUpdateAsync(profile.handle, request)
+ return profileEditNameAsync(profile.handle, request)
.then(() => setProfileContextData(updatedContext))
}
@@ -46,9 +45,10 @@ export const ProfileProvider: FC<{ children: ReactNode }> = ({ children }: { chi
const getAndSetProfileAsync: () => Promise = async () => {
const profile: UserProfile | undefined = await profileGetAsync()
const contextData: ProfileContextData = {
+ changePassword,
initialized: true,
+ isLoggedIn: !!profile,
profile,
- updatePassword,
updateProfile,
}
setProfileContextData(contextData)
diff --git a/src/lib/profile-provider/user-profile.model.ts b/src-ts/lib/profile-provider/user-profile.model.ts
similarity index 100%
rename from src/lib/profile-provider/user-profile.model.ts
rename to src-ts/lib/profile-provider/user-profile.model.ts
diff --git a/src/lib/route-provider/index.ts b/src-ts/lib/route-provider/index.ts
similarity index 85%
rename from src/lib/route-provider/index.ts
rename to src-ts/lib/route-provider/index.ts
index 682dd147b..316699749 100644
--- a/src/lib/route-provider/index.ts
+++ b/src-ts/lib/route-provider/index.ts
@@ -2,4 +2,3 @@ export * from './platform-route.model'
export * from './route-context-data.model'
export { default as routeContext } from './route.context'
export * from './route.provider'
-export * from './route.utils'
diff --git a/src/lib/route-provider/platform-route.model.ts b/src-ts/lib/route-provider/platform-route.model.ts
similarity index 83%
rename from src/lib/route-provider/platform-route.model.ts
rename to src-ts/lib/route-provider/platform-route.model.ts
index 52167ed17..84853343e 100644
--- a/src/lib/route-provider/platform-route.model.ts
+++ b/src-ts/lib/route-provider/platform-route.model.ts
@@ -4,7 +4,9 @@ export interface PlatformRoute {
children: Array
element: JSX.Element
enabled: boolean
+ hide?: boolean
icon?: FC>
+ requireAuth?: boolean
route: string
title: string
}
diff --git a/src-ts/lib/route-provider/require-auth-provider/index.ts b/src-ts/lib/route-provider/require-auth-provider/index.ts
new file mode 100644
index 000000000..715fe70ba
--- /dev/null
+++ b/src-ts/lib/route-provider/require-auth-provider/index.ts
@@ -0,0 +1 @@
+export { default as RequireAuthProvider } from './require-auth.provider'
diff --git a/src-ts/lib/route-provider/require-auth-provider/require-auth.provider.tsx b/src-ts/lib/route-provider/require-auth-provider/require-auth.provider.tsx
new file mode 100644
index 000000000..e6930755c
--- /dev/null
+++ b/src-ts/lib/route-provider/require-auth-provider/require-auth.provider.tsx
@@ -0,0 +1,25 @@
+import { useContext } from 'react'
+
+import { profileContext, ProfileContextData } from '../../profile-provider'
+
+interface RequireAuthProviderProps {
+ children: JSX.Element
+ loginUrl: string
+}
+
+function RequireAuthProvider(props: RequireAuthProviderProps): JSX.Element {
+
+ const profileContextData: ProfileContextData = useContext(profileContext)
+ const { profile, initialized }: ProfileContextData = profileContextData
+
+ // if we have a profile or we're not initialized yet, just return the children
+ if (!initialized || !!profile) {
+ return props.children
+ }
+
+ // redirect to the login page
+ window.location.href = props.loginUrl
+ return <>>
+}
+
+export default RequireAuthProvider
diff --git a/src/lib/route-provider/route-context-data.model.ts b/src-ts/lib/route-provider/route-context-data.model.ts
similarity index 53%
rename from src/lib/route-provider/route-context-data.model.ts
rename to src-ts/lib/route-provider/route-context-data.model.ts
index ac17a3650..f2f223e0c 100644
--- a/src/lib/route-provider/route-context-data.model.ts
+++ b/src-ts/lib/route-provider/route-context-data.model.ts
@@ -7,6 +7,13 @@ export interface RouteContextData {
getChildren: (parent: string) => Array
getChildRoutes: (parent: string) => Array
getPath: (routeTitle: string) => string
+ getPathFromRoute: (route: PlatformRoute) => string
+ getRouteElement: (route: PlatformRoute) => JSX.Element
+ isActiveRoute: (activePath: string, pathName: string, rootPath?: string) => boolean
+ isRootRoute: (activePath: string) => boolean
+ rootLoggedInRoute: string
+ rootLoggedOutRoute: string
toolsRoutes: Array
+ toolsRoutesForNav: Array
utilsRoutes: Array
}
diff --git a/src/lib/route-provider/route.context.tsx b/src-ts/lib/route-provider/route.context.tsx
similarity index 67%
rename from src/lib/route-provider/route.context.tsx
rename to src-ts/lib/route-provider/route.context.tsx
index 6a379c742..c42df28c5 100644
--- a/src/lib/route-provider/route.context.tsx
+++ b/src-ts/lib/route-provider/route.context.tsx
@@ -7,7 +7,14 @@ export const defaultRouteContextData: RouteContextData = {
getChildRoutes: () => [],
getChildren: () => [],
getPath: () => '',
+ getPathFromRoute: () => '',
+ getRouteElement: () => <>>,
+ isActiveRoute: () => false,
+ isRootRoute: () => false,
+ rootLoggedInRoute: '',
+ rootLoggedOutRoute: '',
toolsRoutes: [],
+ toolsRoutesForNav: [],
utilsRoutes: [],
}
diff --git a/src-ts/lib/route-provider/route.provider.tsx b/src-ts/lib/route-provider/route.provider.tsx
new file mode 100644
index 000000000..93363697f
--- /dev/null
+++ b/src-ts/lib/route-provider/route.provider.tsx
@@ -0,0 +1,142 @@
+import { Dispatch, FC, ReactElement, ReactNode, SetStateAction, useEffect, useState } from 'react'
+import { Route } from 'react-router-dom'
+
+import { authUrlLogin } from '../functions'
+
+import { PlatformRoute } from './platform-route.model'
+import { RequireAuthProvider } from './require-auth-provider'
+import { RouteContextData } from './route-context-data.model'
+import { default as routeContext, defaultRouteContextData } from './route.context'
+
+interface RouteProviderProps {
+ children: ReactNode
+ rootLoggedIn: string
+ rootLoggedOut: string
+ toolsRoutes: Array
+ utilsRoutes: Array
+}
+
+export const RouteProvider: FC = (props: RouteProviderProps) => {
+
+ const [routeContextData, setRouteContextData]: [RouteContextData, Dispatch>]
+ = useState(defaultRouteContextData)
+
+ let allRoutes: Array = []
+
+ const getAndSetRoutes: () => void = () => {
+
+ // TODO: try to make these prop names configurable instead of hard-codded
+ const toolsRoutes: Array = props.toolsRoutes.filter(route => route.enabled)
+ const toolsRoutesForNav: Array = toolsRoutes.filter(route => !route.hide)
+ const utilsRoutes: Array = props.utilsRoutes.filter(route => route.enabled)
+ allRoutes = [
+ ...toolsRoutes,
+ ...utilsRoutes,
+ ]
+ const contextData: RouteContextData = {
+ allRoutes,
+ getChildRoutes,
+ getChildren,
+ getPath,
+ getPathFromRoute,
+ getRouteElement,
+ isActiveRoute: isActiveRoute(props.rootLoggedIn, props.rootLoggedOut),
+ isRootRoute: isRootRoute(props.rootLoggedIn, props.rootLoggedOut),
+ rootLoggedInRoute: props.rootLoggedIn,
+ rootLoggedOutRoute: props.rootLoggedOut,
+ toolsRoutes,
+ toolsRoutesForNav,
+ utilsRoutes,
+ }
+ setRouteContextData(contextData)
+ }
+
+ function getChildren(parent: string): Array {
+ return allRoutes
+ .find(route => route.title === parent)
+ ?.children
+ || []
+ }
+
+ function getChildRoutes(parent: string): Array {
+ return getChildren(parent)
+ .map(route => getRouteElement(route))
+ }
+
+ function getPath(routeTitle: string): string {
+ const platformRoute: PlatformRoute = allRoutes.find(route => route.title === routeTitle) as PlatformRoute
+ // if the path has a trailing asterisk, remove it
+ return getPathFromRoute(platformRoute)
+ }
+
+ function getPathFromRoute(route: PlatformRoute): string {
+ return route.route.replace('/*', '')
+ }
+
+ function getRouteElement(route: PlatformRoute): JSX.Element {
+
+ // create the route element
+ const routeElement: JSX.Element = !route.requireAuth
+ ? route.element
+ : (
+
+ {route.element}
+
+ )
+
+ // if the route has children, add the wildcard to the path
+ const path: string = `${route.route}${!route.children ? '' : '/*'}`
+
+ // return the route
+ return (
+
+ )
+ }
+
+ useEffect(() => {
+ getAndSetRoutes()
+ }, [
+ props.toolsRoutes,
+ props.utilsRoutes,
+ ])
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+function isActivePath(activePath: string, pathName: string, rootPath?: string): boolean {
+ return activePath?.startsWith(pathName)
+ && (pathName !== rootPath || activePath === rootPath)
+}
+
+function isActiveRoute(rootLoggedIn: string, rootLoggedOut: string):
+ (activePath: string, pathName: string, rootPath?: string) => boolean {
+
+ return (activePath: string, pathName: string, rootPath?: string) => {
+
+ let isActive: boolean = isActivePath(activePath, pathName, rootPath)
+
+ // if this is the root logged in route,
+ // also check the root logged out route
+ if (!isActive && pathName.startsWith(rootLoggedIn)) {
+ isActive = isActivePath(activePath, rootLoggedOut)
+ }
+
+ return isActive
+ }
+}
+
+function isRootRoute(rootLoggedIn: string, rootLoggedOut: string):
+ (activePath: string) => boolean {
+
+ return (activePath: string) => {
+ return [rootLoggedIn, rootLoggedOut].some(route => activePath === route)
+ }
+}
diff --git a/src-ts/lib/social-links/facebook/Facebook.tsx b/src-ts/lib/social-links/facebook/Facebook.tsx
new file mode 100644
index 000000000..e8b78ab0a
--- /dev/null
+++ b/src-ts/lib/social-links/facebook/Facebook.tsx
@@ -0,0 +1,16 @@
+import { FC } from 'react'
+
+import { SocialIconFacebook } from '../../svgs'
+import { SocialLink } from '../social-link'
+
+const Facebook: FC<{}> = () => {
+
+ return (
+
+ )
+}
+
+export default Facebook
diff --git a/src-ts/lib/social-links/facebook/index.ts b/src-ts/lib/social-links/facebook/index.ts
new file mode 100644
index 000000000..30a4b2461
--- /dev/null
+++ b/src-ts/lib/social-links/facebook/index.ts
@@ -0,0 +1 @@
+export { default as Facebook } from './Facebook'
diff --git a/src-ts/lib/social-links/index.ts b/src-ts/lib/social-links/index.ts
new file mode 100644
index 000000000..175f6f5bf
--- /dev/null
+++ b/src-ts/lib/social-links/index.ts
@@ -0,0 +1,5 @@
+export * from './facebook'
+export * from './instagram'
+export * from './linked-in'
+export * from './twitter'
+export * from './youtube'
diff --git a/src-ts/lib/social-links/instagram/Instagram.tsx b/src-ts/lib/social-links/instagram/Instagram.tsx
new file mode 100644
index 000000000..a7481ff8f
--- /dev/null
+++ b/src-ts/lib/social-links/instagram/Instagram.tsx
@@ -0,0 +1,16 @@
+import { FC } from 'react'
+
+import { SocialIconInstagram } from '../../svgs'
+import { SocialLink } from '../social-link'
+
+const Instagram: FC<{}> = () => {
+
+ return (
+
+ )
+}
+
+export default Instagram
diff --git a/src-ts/lib/social-links/instagram/index.ts b/src-ts/lib/social-links/instagram/index.ts
new file mode 100644
index 000000000..96f094097
--- /dev/null
+++ b/src-ts/lib/social-links/instagram/index.ts
@@ -0,0 +1 @@
+export { default as Instagram } from './Instagram'
diff --git a/src-ts/lib/social-links/linked-in/LinkedIn.tsx b/src-ts/lib/social-links/linked-in/LinkedIn.tsx
new file mode 100644
index 000000000..343c4a5cf
--- /dev/null
+++ b/src-ts/lib/social-links/linked-in/LinkedIn.tsx
@@ -0,0 +1,16 @@
+import { FC } from 'react'
+
+import { SocialIconLinkedIn } from '../../svgs'
+import { SocialLink } from '../social-link'
+
+const LinkedIn: FC<{}> = () => {
+
+ return (
+
+ )
+}
+
+export default LinkedIn
diff --git a/src-ts/lib/social-links/linked-in/index.ts b/src-ts/lib/social-links/linked-in/index.ts
new file mode 100644
index 000000000..1f5a43bd2
--- /dev/null
+++ b/src-ts/lib/social-links/linked-in/index.ts
@@ -0,0 +1 @@
+export { default as LinkedIn } from './LinkedIn'
diff --git a/src-ts/lib/social-links/social-link/SocialLink.tsx b/src-ts/lib/social-links/social-link/SocialLink.tsx
new file mode 100644
index 000000000..c25f755fa
--- /dev/null
+++ b/src-ts/lib/social-links/social-link/SocialLink.tsx
@@ -0,0 +1,22 @@
+import { FC, SVGProps } from 'react'
+interface SocialLinkProps {
+ readonly icon: FC>
+ url: string
+}
+
+const SocialLink: FC = (props: SocialLinkProps) => {
+
+ const Icon: FC> | undefined = props.icon
+
+ if (!Icon) {
+ return <>>
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default SocialLink
diff --git a/src-ts/lib/social-links/social-link/index.ts b/src-ts/lib/social-links/social-link/index.ts
new file mode 100644
index 000000000..626e2cf31
--- /dev/null
+++ b/src-ts/lib/social-links/social-link/index.ts
@@ -0,0 +1 @@
+export { default as SocialLink } from './SocialLink'
diff --git a/src-ts/lib/social-links/twitter/Twitter.tsx b/src-ts/lib/social-links/twitter/Twitter.tsx
new file mode 100644
index 000000000..560c3f5aa
--- /dev/null
+++ b/src-ts/lib/social-links/twitter/Twitter.tsx
@@ -0,0 +1,16 @@
+import { FC } from 'react'
+
+import { SocialIconTwitter } from '../../svgs'
+import { SocialLink } from '../social-link'
+
+const Twitter: FC<{}> = () => {
+
+ return (
+
+ )
+}
+
+export default Twitter
diff --git a/src-ts/lib/social-links/twitter/index.ts b/src-ts/lib/social-links/twitter/index.ts
new file mode 100644
index 000000000..31f555b61
--- /dev/null
+++ b/src-ts/lib/social-links/twitter/index.ts
@@ -0,0 +1 @@
+export { default as Twitter } from './Twitter'
diff --git a/src-ts/lib/social-links/youtube/Youtube.tsx b/src-ts/lib/social-links/youtube/Youtube.tsx
new file mode 100644
index 000000000..6daa79836
--- /dev/null
+++ b/src-ts/lib/social-links/youtube/Youtube.tsx
@@ -0,0 +1,16 @@
+import { FC } from 'react'
+
+import { SocialIconYoutube } from '../../svgs'
+import { SocialLink } from '../social-link'
+
+const Youtube: FC<{}> = () => {
+
+ return (
+
+ )
+}
+
+export default Youtube
diff --git a/src-ts/lib/social-links/youtube/index.ts b/src-ts/lib/social-links/youtube/index.ts
new file mode 100644
index 000000000..d60a90f89
--- /dev/null
+++ b/src-ts/lib/social-links/youtube/index.ts
@@ -0,0 +1 @@
+export { default as Youtube } from './Youtube'
diff --git a/src-ts/lib/styles/_breakpoints.scss b/src-ts/lib/styles/_breakpoints.scss
new file mode 100644
index 000000000..77f41a8d4
--- /dev/null
+++ b/src-ts/lib/styles/_breakpoints.scss
@@ -0,0 +1,45 @@
+@import 'variables/breakpoints';
+
+// TODO: cleanup or convert to mixins?
+
+@media (max-width: #{$xxs-max}) {
+ body {
+ // background-color: red;
+ }
+}
+
+@media (min-width: #{$xs-min}) and (max-width: #{$xs-max}) {
+ body {
+ // background-color: orange;
+ }
+}
+
+@media (min-width: #{$sm-min}) and (max-width: #{$sm-max}){
+ body {
+ // background-color: yellow;
+ }
+}
+
+@media (min-width: #{$md-min}) and (max-width: #{$md-max}){
+ body {
+ // background-color: green;
+ }
+}
+
+@media (min-width: #{$lg-min}) and (max-width: #{$lg-max}){
+ body {
+ // background-color: blue;
+ }
+}
+
+@media (min-width: #{$xl-min}) and (max-width: #{$xl-max}){
+ body {
+ // background-color: purple;
+ }
+}
+
+@media (min-width: #{$xxl-min}){
+ body {
+ // background-color: pink;
+ }
+}
diff --git a/src/lib/button/Button.module.scss b/src-ts/lib/styles/_buttons.scss
similarity index 66%
rename from src/lib/button/Button.module.scss
rename to src-ts/lib/styles/_buttons.scss
index a2fcfa0d7..efdfc86e6 100644
--- a/src/lib/button/Button.module.scss
+++ b/src-ts/lib/styles/_buttons.scss
@@ -1,4 +1,20 @@
-@import '../styles';
+@import 'mixins/icons.mixins';
+@import 'variables/fonts';
+@import 'variables/spacing';
+@import 'variables/palette';
+
+.button-primary-hover {
+ background-color: $turq-140;
+ border-color: $turq-140;
+}
+
+.button-secondary-hover {
+ border-color: $turq-140;
+}
+
+.button-tertiary-hover {
+ color: $turq-140;
+}
.button {
color: $tc-white;
@@ -7,13 +23,17 @@
border: solid $border;
white-space: nowrap;
cursor: pointer;
- margin: $pad-md;
- // extend body ultra small medium and override it
- @extend .body-ultra-small-medium;
+ font-family: $font-roboto;
+ font-weight: $font-weight-bold;
+ font-size: 11px;
line-height: 24px;
-
- &:focus {
- outline: $border solid $turq-140;
+
+ &.primary,
+ &.secondary {
+
+ &:focus {
+ outline: $border solid $turq-140;
+ }
}
&.button-sm {
@@ -22,7 +42,7 @@
}
&.button-md {
- padding: $pad-xs $pad-xl;
+ padding: 3*$border $pad-xl;
font-size: 13px;
}
@@ -34,6 +54,10 @@
&.button-xl {
padding: $pad-md $pad-xxl;
font-size: 16px;
+
+ svg {
+ @include icon-xxl;
+ }
}
&.primary {
@@ -42,8 +66,7 @@
border-color: $turq-160;
&:hover {
- background-color: $turq-140;
- border-color: $turq-140;
+ @extend .button-primary-hover;
}
&:active {
@@ -64,7 +87,7 @@
border-color: $turq-160;
&:hover {
- border-color: $turq-140;
+ @extend .button-secondary-hover;
}
&:active {
@@ -84,7 +107,7 @@
border-color: $tc-white;
&:hover {
- color: $turq-140;
+ @extend .button-tertiary-hover;
}
&:active {
@@ -98,6 +121,10 @@
}
}
+ svg {
+ @include icon-lg;
+ }
+
&.link {
margin: 0;
padding: 0;
@@ -110,10 +137,6 @@
outline: none;
border-radius: 0;
- &:focus {
- outline: $border solid $turq-140;
- }
-
&:hover {
color: $turq-120;
}
@@ -136,6 +159,29 @@
}
&.text {
+ color: $tc-white;
border-color: transparent;
+ font-size: 12px;
+ }
+
+ &.icon {
+ margin: 0;
+ padding: $pad-sm calc($pad-sm - $border-xs) $pad-xs $pad-sm;
+ border: 0;
+ background: none;
+ color: $turq-160;
+
+ &:hover {
+ background-color: $black-10;
+ color: $turq-100;
+
+ &.hover-orange-100 {
+ @extend .orange-100;
+ }
+ }
+
+ &:focus {
+ outline: none;
+ }
}
}
diff --git a/src-ts/lib/styles/_cards.scss b/src-ts/lib/styles/_cards.scss
new file mode 100644
index 000000000..83b0b954f
--- /dev/null
+++ b/src-ts/lib/styles/_cards.scss
@@ -0,0 +1,57 @@
+@import 'variables/palette';
+@import 'variables/spacing';
+
+.card {
+ display: flex;
+ flex-direction: column;
+ padding: $pad-xxl $pad-xxl $pad-xxxxl $pad-xxl;
+ border-radius: $pad-sm;
+ background-color: $black-5;
+
+ &.clickable {
+ background: $tc-grad15;
+ color: $tc-white;
+ cursor: pointer;
+
+ &:hover {
+
+ .button {
+
+ &.primary {
+ @extend .button-primary-hover;
+ }
+
+ &.secondary {
+ @extend .button-secondary-hover;
+ }
+
+ &.tertiary {
+ @extend .button-tertiary-hover;
+ }
+ }
+ }
+ }
+
+ .card-title {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: $pad-sm;
+
+ svg {
+ @include icon-xl;
+ }
+ }
+
+ .button {
+ margin: auto auto 0 0;
+ border: 0;
+ }
+
+ p {
+ word-break: break-all;
+
+ &:last-of-type {
+ margin-bottom: $pad-xl;
+ }
+ }
+}
diff --git a/src-ts/lib/styles/_fonts.scss b/src-ts/lib/styles/_fonts.scss
new file mode 100644
index 000000000..caa8925dd
--- /dev/null
+++ b/src-ts/lib/styles/_fonts.scss
@@ -0,0 +1,63 @@
+@import 'mixins/fonts.mixins';
+
+/* BLACK FONTS */
+.font-black-100 {
+ @include font-black-100;
+}
+.font-black-80 {
+ @include font-black-80;
+}
+.font-black-60 {
+ @include font-black-60;
+}
+.font-black-40 {
+ @include font-black-40;
+}
+.font-tc-white {
+ @include font-tc-white;
+}
+.bg-black-100 {
+ background-color: $black-100;
+}
+
+
+/* RED FONTS */
+.font-red-120 {
+ @include font-red-120;
+}
+.font-red-140 {
+ @include font-red-140;
+}
+
+
+/* TURQUOISE FONTS */
+.font-turq-160 {
+ @include font-turq-160;
+}
+
+
+/* TEAL FONTS */
+.font-teal-140 {
+ @include font-teal-140();
+}
+
+
+/* BLUE FONTS */
+.font-blue-140 {
+ @include font-blue-140;
+}
+.font-link-blue-dark {
+ @include font-link-blue-dark;
+}
+
+
+/* PURPLE FONTS */
+.font-purple-100 {
+ @include font-purple-100;
+}
+.font-purple-120 {
+ @include font-purple-120;
+}
+.font-purple-140 {
+ @include font-purple-140;
+}
diff --git a/src-ts/lib/styles/_includes.scss b/src-ts/lib/styles/_includes.scss
new file mode 100644
index 000000000..f747dfe70
--- /dev/null
+++ b/src-ts/lib/styles/_includes.scss
@@ -0,0 +1,2 @@
+@import './variables';
+@import './mixins';
\ No newline at end of file
diff --git a/src-ts/lib/styles/_layout.scss b/src-ts/lib/styles/_layout.scss
new file mode 100644
index 000000000..c5c45bfb4
--- /dev/null
+++ b/src-ts/lib/styles/_layout.scss
@@ -0,0 +1,29 @@
+@import 'variables';
+@import 'mixins';
+
+html {
+ --header-height: 80px;
+ --footer-height: 51px;
+
+ @include ltemd {
+ --header-height: 64px;
+ }
+
+ @include ltesm {
+ --header-height: 48px;
+ --footer-height: 101px;
+ }
+}
+
+hr {
+ color: $black-10;
+ border: 1px solid;
+ border-radius: 1px;
+ margin: 16px 0px;
+ width: 100%;
+}
+
+#tc-accounts-iframe {
+ border: none;
+ display: none;
+}
diff --git a/src-ts/lib/styles/_modals.scss b/src-ts/lib/styles/_modals.scss
new file mode 100644
index 000000000..a1d3361a1
--- /dev/null
+++ b/src-ts/lib/styles/_modals.scss
@@ -0,0 +1,57 @@
+@import '~react-responsive-modal/styles.css';
+@import 'mixins/breakpoints.mixins';
+@import 'variables/palette';
+@import 'variables/spacing';
+
+.react-responsive-modal-container {
+ display: flex;
+
+ .react-responsive-modal-modal {
+ display: flex;
+ flex-direction: column;
+ width: 100vw;
+ height: 100vh;
+ margin: auto;
+ border-radius: 0;
+ padding: $pad-xxl $pad-xxxxl $pad-xxxxl;
+
+ &.modal-md {
+ width: 450px;
+ }
+
+ &.modal-lg {
+ width: 700px;
+ }
+
+ @include ltemd {
+ max-width: 450px;
+ }
+
+ @include gtemd {
+ width: auto;
+ height: auto;
+ border-radius: $pad-sm;
+ max-height: 93vh;
+ min-width: 450px;
+ }
+
+ .react-responsive-modal-closeButton {
+ top: $pad-xl;
+ padding: $pad-xs;
+ border-radius: 50%;
+
+ svg {
+ fill: $turq-160;
+
+ @include gtemd {
+ fill: $black-100;
+ }
+ }
+
+ &:focus {
+ background-color: $black-10;
+ outline: 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src-ts/lib/styles/_reset.scss b/src-ts/lib/styles/_reset.scss
new file mode 100644
index 000000000..6351952a5
--- /dev/null
+++ b/src-ts/lib/styles/_reset.scss
@@ -0,0 +1,123 @@
+*, *::before, *:after {
+ box-sizing: inherit;
+}
+
+html {
+ box-sizing: border-box;
+}
+
+textarea {
+ resize: vertical;
+}
+
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {
+ color: #a0aec0;
+}
+
+input::-ms-input-placeholder, textarea::-ms-input-placeholder {
+ color: #a0aec0;
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: #a0aec0;
+}
+
+button,
+[role="button"] {
+ cursor: pointer;
+ border: 0 none;
+ background: none;
+ outline: none;
+ appearance: none;
+}
+
+label {
+ cursor: inherit;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-size: inherit;
+ font-weight: inherit;
+ margin: 0;
+}
+
+/**
+* Reset links to optimize for opt-in styling instead of
+* opt-out.
+*/
+
+a, a:hover {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+/**
+* Reset form element properties that are easy to forget to
+* style explicitly so you don't inadvertently introduce
+* styles that deviate from your design system. These styles
+* supplement a partial reset that is already applied by
+* normalize.css.
+*/
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ padding: 0;
+ line-height: inherit;
+ color: inherit;
+}
+
+/**
+* Make replaced elements display: block by default as that's
+* the behavior you want almost all of the time. Inspired by
+* CSS Remedy, with svg added as well.
+*
+* https://github.com/mozdevs/cssremedy/issues/14
+*/
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+ display: block;
+}
+
+/**
+* Constrain images and videos to the parent width and preserve
+* their instrinsic aspect ratio.
+*
+* https://github.com/mozdevs/cssremedy/issues/14
+*/
+
+img,
+video {
+ max-width: 100%;
+ height: auto;
+}
+
+ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+
+ &.list {
+ list-style: disc;
+ padding-left: 24px;
+ }
+}
\ No newline at end of file
diff --git a/src-ts/lib/styles/_typography.scss b/src-ts/lib/styles/_typography.scss
new file mode 100644
index 000000000..5454c7314
--- /dev/null
+++ b/src-ts/lib/styles/_typography.scss
@@ -0,0 +1,203 @@
+@import 'variables/breakpoints';
+@import 'variables/fonts';
+@import 'variables/palette';
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: $font-roboto;
+ color: $black-100;
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: 0;
+ min-width: $xxs-max;
+}
+
+h1,
+h2,
+h3,
+h4 {
+ font-family: $font-barlow-condensed;
+ font-weight: $font-weight-semibold;
+ text-transform: uppercase;
+ letter-spacing: 0;
+ margin: 0;
+ padding: 0;
+ border: 0 none;
+
+ &.details {
+ font-family: $font-barlow;
+ }
+}
+
+h1 {
+ font-family: $font-barlow-condensed;
+ font-size: 34px;
+ line-height: 32px;
+
+ &.details {
+ font-family: $font-barlow;
+ }
+}
+
+h2 {
+ font-size: 24px;
+ line-height: 28px;
+}
+
+h3 {
+ font-size: 22px;
+ line-height: 26px;
+}
+
+h4 {
+ font-size: 18px;
+ line-height: 22px;
+}
+
+/* Tabs */
+.large-tab {
+ font-family: $font-barlow-condensed;
+ font-weight: $font-weight-medium;
+ font-size: 20px;
+ line-height: 16px;
+ text-transform: uppercase;
+}
+
+.medium-tab {
+ font-family: $font-barlow;
+ font-weight: $font-weight-semibold;
+ text-transform: uppercase;
+ font-size: 14px;
+ line-height: 20px;
+}
+
+.small-tab {
+ font-family: $font-barlow;
+ font-weight: $font-weight-medium;
+ font-size: 11px;
+ line-height: 14px;
+}
+
+/* Subtitles */
+.large-subtitle,
+.large-subtitle-bold {
+ font-size: 16px;
+ line-height: 20px;
+}
+
+.large-subtitle {
+ font-weight: $font-weight-medium;
+}
+
+.large-subtitle-bold {
+ font-weight: $font-weight-bold;
+}
+
+.medium-subtitle {
+ font-weight: $font-weight-medium;
+ font-size: 14px;
+ line-height: 16px;
+ letter-spacing: .5px;
+}
+
+.large-tab {
+ font-family: $font-barlow-condensed;
+ font-size: 20px;
+ line-height: 16px;
+ text-transform: uppercase;
+ font-weight: $font-weight-medium;
+}
+
+/* Body Text */
+
+.body-main {
+ font-family: $font-roboto;
+ font-size: 16px;
+ line-height: 24px;
+}
+
+.body-large {
+ font-family: $font-roboto;
+ font-size: 24px;
+ line-height: 32px;
+}
+
+.body-large-medium {
+ font-weight: $font-weight-medium;
+ font-size: 24px;
+ line-height: 32px;
+ text-align: center;
+}
+
+.body-medium-medium {
+ font-weight: $font-weight-semibold;
+ font-size: 20px;
+ line-height: 26px;
+}
+
+.body-medium-bold {
+ font-weight: $font-weight-bold;
+ font-family: $font-barlow;
+ padding: 0;
+ font-size: 20px;
+ line-height: 22px;
+}
+
+.body-small {
+ font-size: 14px !important;
+ line-height: 22px !important;
+ font-weight: $font-weight-normal;
+}
+
+.body-small-medium {
+ @extend .body-small;
+ font-weight: $font-weight-medium;
+ letter-spacing: 0;
+}
+
+.body-ultra-small,
+.body-ultra-small-medium {
+ font-size: 12px;
+ line-height: 18px;
+ font-weight: $font-weight-normal;
+}
+
+.body-ultra-small-medium {
+ font-weight: $font-weight-bold;
+}
+
+/* Ultra small */
+.ultra-small,
+.ultra-small-medium,
+.ultra-small-bold {
+ font-family: $font-roboto;
+ font-size: 11px;
+ line-height: 16px;
+}
+
+.ultra-small {
+ font-weight: $font-weight-normal;
+}
+
+.ultra-small-medium {
+ font-weight: $font-weight-medium;
+}
+
+.ultra-small-bold {
+ font-weight: $font-weight-bold;
+}
+
+.overline {
+ font-size: 12px;
+ font-weight: $font-weight-bold;
+ line-height: 16px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.label {
+ font-weight: $font-weight-semibold;
+ font-size: 11px;
+ line-height: 14px;
+}
diff --git a/src/lib/styles/index.scss b/src-ts/lib/styles/index.scss
similarity index 73%
rename from src/lib/styles/index.scss
rename to src-ts/lib/styles/index.scss
index f784e1968..650fd5bcb 100644
--- a/src/lib/styles/index.scss
+++ b/src-ts/lib/styles/index.scss
@@ -1,12 +1,10 @@
+@import 'reset';
@import '~react-toastify/dist/ReactToastify.min.css';
@import 'breakpoints';
@import 'buttons';
+@import 'cards';
@import 'fonts';
-@import 'icons';
@import 'layout';
@import 'modals';
-@import 'palette';
@import 'typography';
-
-/* TODO: add reset css */
diff --git a/src/lib/styles/_breakpoints.scss b/src-ts/lib/styles/mixins/_breakpoints.mixins.scss
similarity index 58%
rename from src/lib/styles/_breakpoints.scss
rename to src-ts/lib/styles/mixins/_breakpoints.mixins.scss
index 7be5e90e6..66d0db6de 100644
--- a/src/lib/styles/_breakpoints.scss
+++ b/src-ts/lib/styles/mixins/_breakpoints.mixins.scss
@@ -1,16 +1,4 @@
-$xxs-max: 320px;
-$xs-min: 321px;
-$xs-max: 375px;
-$sm-min: 376px;
-$sm-max: 464px;
-$md-min: 465px;
-$md-max: 744px;
-$lg-min: 745px;
-$lg-max: 984px;
-$xl-min: 985px;
-$xl-max: 1439px;
-$xxl-min: 1440px;
-
+@import '../variables/breakpoints';
// Usage:
// .example {
// width: 1000px;
@@ -101,46 +89,4 @@ $xxl-min: 1440px;
@media (min-width: #{$xxl-min}){
@content;
}
-}
-
-@media (max-width: #{$xxs-max}) {
- body {
- // background-color: red;
- }
-}
-
-@media (min-width: #{$xs-min}) and (max-width: #{$xs-max}) {
- body {
- // background-color: orange;
- }
-}
-
-@media (min-width: #{$sm-min}) and (max-width: #{$sm-max}){
- body {
- // background-color: yellow;
- }
-}
-
-@media (min-width: #{$md-min}) and (max-width: #{$md-max}){
- body {
- // background-color: green;
- }
-}
-
-@media (min-width: #{$lg-min}) and (max-width: #{$lg-max}){
- body {
- // background-color: blue;
- }
-}
-
-@media (min-width: #{$xl-min}) and (max-width: #{$xl-max}){
- body {
- // background-color: purple;
- }
-}
-
-@media (min-width: #{$xxl-min}){
- body {
- // background-color: pink;
- }
-}
+}
\ No newline at end of file
diff --git a/src-ts/lib/styles/mixins/_fonts.mixins.scss b/src-ts/lib/styles/mixins/_fonts.mixins.scss
new file mode 100644
index 000000000..77bb7e72b
--- /dev/null
+++ b/src-ts/lib/styles/mixins/_fonts.mixins.scss
@@ -0,0 +1,101 @@
+@import '../variables/fonts';
+@import '../variables/palette';
+
+/* FONT-FAMILIES */
+@mixin font-roboto {
+ font-family: $font-roboto;
+}
+
+@mixin font-barlow-condensed {
+ font-family: $font-barlow-condensed;
+}
+
+@mixin font-barlow {
+ font-family: $font-barlow;
+}
+
+
+/* FONT WEIGHTS */
+@mixin font-weight-normal {
+ font-weight: $font-weight-normal;
+}
+
+@mixin font-weight-medium {
+ font-weight: $font-weight-medium;
+}
+
+@mixin font-weight-semibold {
+ font-weight: $font-weight-semibold;
+}
+
+@mixin font-weight-bold {
+ font-weight: $font-weight-bold;
+}
+
+
+/* BLACK FONTS */
+@mixin font-black-100 {
+ color: $black-100;
+}
+
+@mixin font-black-80 {
+ color: $black-80;
+}
+
+@mixin font-black-60 {
+ color: $black-60;
+}
+
+@mixin font-black-40 {
+ color: $black-40;
+}
+
+@mixin font-tc-white {
+ color: $tc-white;
+}
+
+
+/* RED FONTS */
+@mixin font-red-120 {
+ color: $red-120;
+}
+
+@mixin font-red-140 {
+ color: $red-140;
+}
+
+
+/* TURQUOISE FONTS */
+@mixin font-turq-160 {
+ color: $turq-160;
+}
+
+
+/* TEAL FONTS */
+@mixin font-teal-140 {
+ color: $teal-140;
+}
+
+
+/* BLUE FONTS */
+@mixin font-blue-140 {
+ color: $blue-140;
+}
+
+@mixin font-link-blue-dark {
+ color: $link-blue-dark;
+}
+
+
+/* PURPLE FONTS */
+@mixin font-purple-100 {
+ color: $purple-100;
+}
+
+@mixin font-purple-120 {
+ color: $purple-120;
+}
+
+@mixin font-purple-140 {
+ color: $purple-140;
+}
\ No newline at end of file
diff --git a/src/lib/styles/_icons.scss b/src-ts/lib/styles/mixins/_icons.mixins.scss
similarity index 62%
rename from src/lib/styles/_icons.scss
rename to src-ts/lib/styles/mixins/_icons.mixins.scss
index 42dc0f05f..2e86e77a6 100644
--- a/src/lib/styles/_icons.scss
+++ b/src-ts/lib/styles/mixins/_icons.mixins.scss
@@ -1,4 +1,9 @@
-@import 'layout';
+@import '../variables/spacing';
+
+@mixin icon-sm {
+ height: $pad-sm;
+ width: $pad-sm;
+}
@mixin icon-md {
height: $pad-md;
@@ -10,6 +15,11 @@
width: $pad-lg;
}
+@mixin icon-xl {
+ height: $pad-xl;
+ width: $pad-xl;
+}
+
@mixin icon-xxl {
height: $pad-xxl;
width: $pad-xxl;
diff --git a/src-ts/lib/styles/mixins/_layout.mixins.scss b/src-ts/lib/styles/mixins/_layout.mixins.scss
new file mode 100644
index 000000000..421726081
--- /dev/null
+++ b/src-ts/lib/styles/mixins/_layout.mixins.scss
@@ -0,0 +1,16 @@
+@import '../variables/spacing';
+
+@mixin pagePaddings {
+ padding-left: $pad-xxxxl;
+ padding-right: $pad-xxxxl;
+
+ @include ltemd {
+ padding-left: $pad-xxl;
+ padding-right: $pad-xxl;
+ }
+
+ @include xxs {
+ padding-left: $pad-lg;
+ padding-right: $pad-lg;
+ }
+}
\ No newline at end of file
diff --git a/src-ts/lib/styles/mixins/_typography.mixins.scss b/src-ts/lib/styles/mixins/_typography.mixins.scss
new file mode 100644
index 000000000..870564c2f
--- /dev/null
+++ b/src-ts/lib/styles/mixins/_typography.mixins.scss
@@ -0,0 +1,13 @@
+@mixin text-ellipsis {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+@mixin text-clamp($noOfLines: 1) {
+ display: -webkit-box;
+ -webkit-line-clamp: $noOfLines;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/src-ts/lib/styles/mixins/index.scss b/src-ts/lib/styles/mixins/index.scss
new file mode 100644
index 000000000..f7f4fcf14
--- /dev/null
+++ b/src-ts/lib/styles/mixins/index.scss
@@ -0,0 +1,5 @@
+@import './breakpoints.mixins';
+@import './fonts.mixins';
+@import './icons.mixins';
+@import './layout.mixins';
+@import './typography.mixins';
\ No newline at end of file
diff --git a/src-ts/lib/styles/variables/_breakpoints.scss b/src-ts/lib/styles/variables/_breakpoints.scss
new file mode 100644
index 000000000..e56238caa
--- /dev/null
+++ b/src-ts/lib/styles/variables/_breakpoints.scss
@@ -0,0 +1,12 @@
+$xxs-max: 320px;
+$xs-min: 321px;
+$xs-max: 375px;
+$sm-min: 376px;
+$sm-max: 464px;
+$md-min: 465px;
+$md-max: 744px;
+$lg-min: 745px;
+$lg-max: 984px;
+$xl-min: 985px;
+$xl-max: 1439px;
+$xxl-min: 1440px;
diff --git a/src-ts/lib/styles/variables/_fonts.scss b/src-ts/lib/styles/variables/_fonts.scss
new file mode 100644
index 000000000..f88d7dc7d
--- /dev/null
+++ b/src-ts/lib/styles/variables/_fonts.scss
@@ -0,0 +1,9 @@
+/* FONT-FAMILIES */
+$font-roboto: "Roboto", Arial, Helvetica, sans-serif;
+$font-barlow: "Barlow", Arial, Helvetica, sans-serif;
+$font-barlow-condensed: "Barlow Condensed", Arial, Helvetica, sans-serif;
+
+$font-weight-normal: 400;
+$font-weight-medium: 500;
+$font-weight-semibold: 600;
+$font-weight-bold: 700;
diff --git a/src-ts/lib/styles/variables/_layouts.scss b/src-ts/lib/styles/variables/_layouts.scss
new file mode 100644
index 000000000..ea6bc0687
--- /dev/null
+++ b/src-ts/lib/styles/variables/_layouts.scss
@@ -0,0 +1,5 @@
+$header-height: var(--header-height, 80px);
+$footer-height: var(--footer-height, 51px);
+
+$max-content-width: 1440px;
+$content-height: calc(100vh - $header-height); // TO BE ADDED WHEN FOOTER IS MERGED: `- $footer-height`
diff --git a/src/lib/styles/_palette.scss b/src-ts/lib/styles/variables/_palette.scss
similarity index 80%
rename from src/lib/styles/_palette.scss
rename to src-ts/lib/styles/variables/_palette.scss
index cb06e51ab..0ce32dbae 100644
--- a/src/lib/styles/_palette.scss
+++ b/src-ts/lib/styles/variables/_palette.scss
@@ -9,9 +9,41 @@ $black-10: #E9E9E9;
$black-5: #F4F4F4;
$black-2: #FBFBFB;
+.black-20 {
+ color: $black-20 !important;
+}
+
+.black-60 {
+ color: $black-60 !important;
+}
+
+.black-100 {
+ color: $black-100 !important;
+}
+
+.bg-black-5 {
+ background-color: $black-5;
+}
+
+.bg-black-10 {
+ background-color: $black-10;
+}
+
+.bg-black-20 {
+ background-color: $black-20;
+}
+
+.bg-black-100 {
+ background-color: $black-100;
+}
+
+
/* WHITE */
$tc-white: #FFFFFF;
+.bg-tc-white {
+ background-color: $tc-white;
+}
/* RED */
$red-100: #EF3A3A;
@@ -32,6 +64,10 @@ $orange-25: #FFF0CE;
$orange-120: #E2AF3B;
$orange-140: #B98F31;
+.orange-100 {
+ color: $orange-100 !important;
+}
+
/* YELLOW */
$yellow-100: #F9F649;
@@ -77,14 +113,17 @@ $teal-140: #227681;
/* BLUE */
+$blue-110: #0A7AC0;
$blue-100: #2C95D7;
$blue-75: #50ADE8;
$blue-50: #83C5EE;
$blue-25: #BAE1F9;
-$blue-15: #EAF6FD;
+$blue-15: #D6EDFC;
+$blue-10: #EAF6FD;
// dark
$blue-120: #2984BD;
$blue-140: #16679A;
+$blue-160: #05456D;
$link-blue-dark: #0D61BF;
$link-blue-light: #5FB7EE;
@@ -98,6 +137,10 @@ $purple-25: #E6CFF1;
$purple-120: #8231A9;
$purple-140: #652385;
+.bg-purple-100 {
+ background-color: $purple-100;
+}
+
/* ORANGE LEGACY */
$legacy-100: #FD7D01;
@@ -113,7 +156,7 @@ $handle-grey: $black-100;
$handle-green: #2D7E2D;
$handle-blue: #616BD5;
$handle-yellow: #F2C900;
-$handle-red: #EF3A3A;
+$handle-red: #EF3A3A;
/* GOLD */
@@ -130,7 +173,7 @@ $silver-3: #554E48;
/* BRONZE */
$bronze-1: #D98F64;
-$bronze-2: #854E29;
+$bronze-2: #854E29;
$bronze-3: #733D17;
@@ -149,6 +192,7 @@ $tc-grad11: linear-gradient(90deg, #3023AE 0%, #C86DD7 100%);
$tc-grad12: linear-gradient(90deg, #652385 0%, #8C384C 100%);
$tc-grad13: linear-gradient(90deg, #219174 0%, #B98F31 100%);
$tc-grad14: linear-gradient(0deg, #880152 0%, #BE4A1D 100%);
+$tc-grad15: linear-gradient(84.45deg, $blue-160 2.12%, $blue-110 97.43%);
/* OPACITY */
diff --git a/src/lib/styles/_layout.scss b/src-ts/lib/styles/variables/_spacing.scss
similarity index 58%
rename from src/lib/styles/_layout.scss
rename to src-ts/lib/styles/variables/_spacing.scss
index 42e58ec00..06ebb1628 100644
--- a/src/lib/styles/_layout.scss
+++ b/src-ts/lib/styles/variables/_spacing.scss
@@ -1,4 +1,4 @@
-$header-height: 80px;
+@import './layouts';
/* Left Col includes the header logo link and the sections container */
$left-col-width-md: $header-height;
@@ -18,38 +18,3 @@ $pad-xl: calc(5 * $pad-xs); // 20
$pad-xxl: calc(6 * $pad-xs); // 24
$pad-xxxl: calc(7 * $pad-xs); // 28
$pad-xxxxl: calc(8 * $pad-xs); // 32
-$pad-content-lg: calc($pad-xxl + $pad-xxxxl);
-
-@mixin content-height {
- height: calc(100vh - $header-height);
-}
-
-
-.pad-xs {
- padding: $pad-xs;
-}
-
-.pad-sm {
- padding: $pad-sm;
-}
-
-.pad-md {
- padding: $pad-md;
-}
-
-.pad-lg {
- padding: $pad-lg;
-}
-
-.pad-xl {
- padding: $pad-xl;
-}
-
-.pad-xxl {
- padding: $pad-xxl;
-}
-
-#tc-accounts-iframe {
- border: none;
- display: none;
-}
diff --git a/src-ts/lib/styles/variables/index.scss b/src-ts/lib/styles/variables/index.scss
new file mode 100644
index 000000000..d6491d396
--- /dev/null
+++ b/src-ts/lib/styles/variables/index.scss
@@ -0,0 +1,5 @@
+@import './breakpoints';
+@import './fonts';
+@import './layouts';
+@import './palette';
+@import './spacing';
\ No newline at end of file
diff --git a/src-ts/lib/svgs/activetab-tip-icon.svg b/src-ts/lib/svgs/activetab-tip-icon.svg
new file mode 100755
index 000000000..39e0bc2a0
--- /dev/null
+++ b/src-ts/lib/svgs/activetab-tip-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src-ts/lib/svgs/index.ts b/src-ts/lib/svgs/index.ts
new file mode 100644
index 000000000..6a5e5da55
--- /dev/null
+++ b/src-ts/lib/svgs/index.ts
@@ -0,0 +1,23 @@
+// SEE https://heroicons.com/ for options
+import * as IconOutline from '@heroicons/react/outline'
+import * as IconSolid from '@heroicons/react/solid'
+
+import { ReactComponent as ActiveTabTipIcon } from './activetab-tip-icon.svg'
+import { ReactComponent as LogoIcon } from './logo.svg'
+import { ReactComponent as SocialIconFacebook } from './social-fb-icon.svg'
+import { ReactComponent as SocialIconInstagram } from './social-insta-icon.svg'
+import { ReactComponent as SocialIconLinkedIn } from './social-linkedin-icon.svg'
+import { ReactComponent as SocialIconTwitter } from './social-tw-icon.svg'
+import { ReactComponent as SocialIconYoutube } from './social-yt-icon.svg'
+import { ReactComponent as TooltipArrowIcon } from './tooltip-arrow.svg'
+
+export { ActiveTabTipIcon }
+export { IconOutline }
+export { IconSolid }
+export { LogoIcon }
+export { SocialIconFacebook }
+export { SocialIconInstagram }
+export { SocialIconLinkedIn }
+export { SocialIconTwitter }
+export { SocialIconYoutube }
+export { TooltipArrowIcon }
diff --git a/src/lib/svgs/logo.svg b/src-ts/lib/svgs/logo.svg
similarity index 100%
rename from src/lib/svgs/logo.svg
rename to src-ts/lib/svgs/logo.svg
diff --git a/src-ts/lib/svgs/social-fb-icon.svg b/src-ts/lib/svgs/social-fb-icon.svg
new file mode 100644
index 000000000..6fe6d3fdd
--- /dev/null
+++ b/src-ts/lib/svgs/social-fb-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src-ts/lib/svgs/social-insta-icon.svg b/src-ts/lib/svgs/social-insta-icon.svg
new file mode 100644
index 000000000..6c51702ce
--- /dev/null
+++ b/src-ts/lib/svgs/social-insta-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src-ts/lib/svgs/social-linkedin-icon.svg b/src-ts/lib/svgs/social-linkedin-icon.svg
new file mode 100644
index 000000000..0ce93b8a2
--- /dev/null
+++ b/src-ts/lib/svgs/social-linkedin-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src-ts/lib/svgs/social-tw-icon.svg b/src-ts/lib/svgs/social-tw-icon.svg
new file mode 100644
index 000000000..48272e01d
--- /dev/null
+++ b/src-ts/lib/svgs/social-tw-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src-ts/lib/svgs/social-yt-icon.svg b/src-ts/lib/svgs/social-yt-icon.svg
new file mode 100644
index 000000000..2c28fbc23
--- /dev/null
+++ b/src-ts/lib/svgs/social-yt-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src-ts/lib/svgs/tooltip-arrow.svg b/src-ts/lib/svgs/tooltip-arrow.svg
new file mode 100644
index 000000000..18d794edc
--- /dev/null
+++ b/src-ts/lib/svgs/tooltip-arrow.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src-ts/lib/table/Table.module.scss b/src-ts/lib/table/Table.module.scss
new file mode 100644
index 000000000..c1fcfd3d7
--- /dev/null
+++ b/src-ts/lib/table/Table.module.scss
@@ -0,0 +1,92 @@
+@use '../styles/typography';
+@import '../styles/includes';
+
+.table-wrap {
+ width: 100%;
+ overflow: auto;
+}
+
+.table {
+ border-collapse: separate;
+ border-spacing: 0;
+ width: 100%;
+
+ .tr {
+
+ &.clickable {
+ position: relative;
+ cursor: pointer;
+
+ td {
+ border: 1px solid transparent;
+ }
+
+ &:hover {
+
+ td {
+ border-top-color: $turq-160;
+ border-bottom-color: $turq-160;
+
+ &:first-child {
+ border-left-color: $turq-160;
+ }
+
+ &:last-child {
+ border-right-color: $turq-160;
+ }
+ }
+ }
+ }
+
+ .th {
+ @extend .small-tab;
+ padding: $pad-lg;
+ white-space: nowrap;
+ text-align: left;
+ text-transform: uppercase;
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ .header-container {
+ display: flex;
+ align-items: center;
+
+ &.money {
+ text-align: right;
+ justify-content: flex-end;
+
+ &.sortable {
+ margin-right: -29px;
+ }
+ }
+ }
+
+ .tooltip {
+ display: inline-block;
+ margin-left: $pad-xs;
+ position: relative;
+ top: 2px;
+
+ :global(.tooltip-icon) {
+
+ svg {
+ @include icon-md;
+ }
+ }
+ }
+ }
+
+ &:nth-child(2n + 1) {
+
+ td {
+ background-color: $tc-white;
+ }
+ }
+ }
+}
+
+.tootlipBody {
+ min-width: 200px;
+}
diff --git a/src-ts/lib/table/Table.tsx b/src-ts/lib/table/Table.tsx
new file mode 100644
index 000000000..fe1228886
--- /dev/null
+++ b/src-ts/lib/table/Table.tsx
@@ -0,0 +1,159 @@
+import classNames from 'classnames'
+import { Dispatch, MouseEvent, SetStateAction, useEffect, useState } from 'react'
+
+import { Sort } from '../pagination'
+import '../styles/_includes.scss'
+import { IconOutline } from '../svgs'
+import { Tooltip } from '../tooltip'
+
+import { TableCell } from './table-cell'
+import { TableColumn } from './table-column.model'
+import { tableGetDefaultSort, tableGetSorted } from './table-functions'
+import { TableSort } from './table-sort'
+import styles from './Table.module.scss'
+
+interface TableProps {
+ readonly columns: ReadonlyArray>
+ readonly data: ReadonlyArray
+ readonly onRowClick?: (data: T) => void
+}
+
+interface DefaultSortDirectionMap {
+ [columnProperty: string]: 'asc' | 'desc'
+}
+
+const Table: (props: TableProps) => JSX.Element
+ = (props: TableProps) => {
+
+ const [sort, setSort]: [Sort | undefined, Dispatch>]
+ = useState(tableGetDefaultSort(props.columns))
+ const [defaultSortDirectionMap, setDefaultSortDirectionMap]: [DefaultSortDirectionMap | undefined, Dispatch>]
+ = useState()
+ const [sortedData, setSortData]: [ReadonlyArray, Dispatch>>]
+ = useState>(props.data)
+
+ useEffect(() => {
+
+ if (!defaultSortDirectionMap) {
+ const map: DefaultSortDirectionMap = {}
+ props.columns
+ .filter(col => !!col.propertyName)
+ .forEach(col => map[col.propertyName as string] = col.defaultSortDirection || 'asc')
+ setDefaultSortDirectionMap(map)
+ }
+
+ setSortData(tableGetSorted(props.data, props.columns, sort))
+ },
+ [
+ defaultSortDirectionMap,
+ sort,
+ props.data,
+ ])
+
+ function toggleSort(fieldName: string): void {
+
+ // if we don't have anything to sort by, we shouldn't be here
+ if (!sort) {
+ return
+ }
+
+ // get the sort direction
+ const direction: 'asc' | 'desc' = fieldName === sort.fieldName
+ // this is the current sort, so just toggle it
+ ? sort.direction === 'asc' ? 'desc' : 'asc'
+ // get the default sort for the field... this will never be undefined
+ : (defaultSortDirectionMap as DefaultSortDirectionMap)[fieldName]
+
+ const newSort: Sort = {
+ direction,
+ fieldName,
+ }
+ setSort(newSort)
+ }
+
+ const headerRow: Array = props.columns
+ .map((col, index) => {
+ const isSortable: boolean = !!col.propertyName
+ const isCurrentlySorted: boolean = isSortable && col.propertyName === sort?.fieldName
+ const colorClass: string = isCurrentlySorted ? 'black-100' : 'black-60'
+ const sortableClass: string | undefined = isSortable ? styles.sortable : undefined
+ return (
+
+
+ {col.label}
+ {!!col.tooltip && (
+
+ }
+ />
+
+ )}
+
+
+ |
+ )
+ })
+
+ const rowCells: Array = sortedData
+ .map((data, index) => {
+
+ function onRowClick(event: MouseEvent): void {
+ event.preventDefault()
+ props.onRowClick?.(data)
+ }
+
+ // get the cells in the row
+ const cells: Array = props.columns
+ .map((col, colIndex) => {
+ return (
+
+ )
+ })
+
+ // return the entire row
+ return (
+
+ {cells}
+
+ )
+ })
+
+ return (
+ /* TODO: sticky header */
+
+
+
+
+ {headerRow}
+
+
+
+ {rowCells}
+
+
+
+ )
+ }
+
+export default Table
diff --git a/src-ts/lib/table/index.ts b/src-ts/lib/table/index.ts
new file mode 100644
index 000000000..d7b124c44
--- /dev/null
+++ b/src-ts/lib/table/index.ts
@@ -0,0 +1,2 @@
+export * from './table-column.model'
+export { default as Table } from './Table'
diff --git a/src-ts/lib/table/table-cell.type.ts b/src-ts/lib/table/table-cell.type.ts
new file mode 100644
index 000000000..b4739c574
--- /dev/null
+++ b/src-ts/lib/table/table-cell.type.ts
@@ -0,0 +1 @@
+export type TableCellType = 'action' | 'date' | 'element' | 'money' | 'text'
diff --git a/src-ts/lib/table/table-cell/TableCell.module.scss b/src-ts/lib/table/table-cell/TableCell.module.scss
new file mode 100644
index 000000000..9a4637ec4
--- /dev/null
+++ b/src-ts/lib/table/table-cell/TableCell.module.scss
@@ -0,0 +1,38 @@
+@use '../../styles/typography';
+@import '../../styles/variables';
+
+.td {
+ max-width: 450px;
+ @extend .medium-subtitle;
+ padding: $pad-lg;
+ background: $black-5;
+ word-break: break-word;
+ vertical-align: top;
+
+ &:first-child {
+ border-radius: $pad-sm 0 0 $pad-sm;
+ }
+
+ &:last-child {
+ border-radius: 0 $pad-sm $pad-sm 0;
+ }
+
+ &:nth-last-child(2) {
+ text-align: center;
+ }
+
+ &.action {
+ text-align: center;
+ padding: $pad-sm;
+ }
+
+ &.date,
+ &.money,
+ &.text {
+ white-space: nowrap;
+ }
+
+ &.money {
+ text-align: right;
+ }
+}
diff --git a/src-ts/lib/table/table-cell/TableCell.tsx b/src-ts/lib/table/table-cell/TableCell.tsx
new file mode 100644
index 000000000..f93b5ab6f
--- /dev/null
+++ b/src-ts/lib/table/table-cell/TableCell.tsx
@@ -0,0 +1,70 @@
+import classNames from 'classnames'
+import { MouseEvent } from 'react'
+
+import { textFormatDateLocaleShortString, textFormatMoneyLocaleString } from '../../functions'
+import { TableCellType } from '../table-cell.type'
+
+import styles from './TableCell.module.scss'
+
+interface TableCellProps {
+ readonly data: T
+ readonly index: number
+ readonly propertyName?: string
+ readonly renderer?: (data: T) => JSX.Element | undefined
+ readonly type: TableCellType
+}
+
+const TableCell: (props: TableCellProps) => JSX.Element
+ = (props: TableCellProps) => {
+
+ let data: string | JSX.Element | undefined
+ switch (props.type) {
+ case 'date':
+ data = textFormatDateLocaleShortString(props.data[props.propertyName as string] as Date)
+ break
+
+ case 'action':
+ case 'element':
+ data = props.renderer?.(props.data)
+ break
+
+ case 'money':
+ data = textFormatMoneyLocaleString(props.data[props.propertyName as string])
+ break
+
+ default:
+ data = props.data[props.propertyName as string] as string
+ break
+ }
+
+ function onClick(event: MouseEvent): void {
+ if (props.type !== 'action') {
+ return
+ }
+ // this is an action table cell, so stop propagation
+ event.preventDefault()
+ event.stopPropagation()
+ }
+
+ const classes: string = classNames(
+ styles.td,
+ styles[props.type],
+ !data ? styles.empty : undefined,
+ )
+
+ return (
+
+
+ {data}
+
+ |
+ )
+ }
+
+export default TableCell
diff --git a/src-ts/lib/table/table-cell/index.ts b/src-ts/lib/table/table-cell/index.ts
new file mode 100644
index 000000000..57199939d
--- /dev/null
+++ b/src-ts/lib/table/table-cell/index.ts
@@ -0,0 +1 @@
+export { default as TableCell } from './TableCell'
diff --git a/src-ts/lib/table/table-column.model.ts b/src-ts/lib/table/table-column.model.ts
new file mode 100644
index 000000000..4323da523
--- /dev/null
+++ b/src-ts/lib/table/table-column.model.ts
@@ -0,0 +1,11 @@
+import { TableCellType } from './table-cell.type'
+
+export interface TableColumn {
+ readonly defaultSortDirection?: 'asc' | 'desc'
+ readonly isDefaultSort?: boolean
+ readonly label?: string
+ readonly propertyName?: string
+ readonly renderer?: (data: T, params?: any) => JSX.Element | undefined
+ readonly tooltip?: string
+ readonly type: TableCellType
+}
diff --git a/src-ts/lib/table/table-functions/index.ts b/src-ts/lib/table/table-functions/index.ts
new file mode 100644
index 000000000..6e705a995
--- /dev/null
+++ b/src-ts/lib/table/table-functions/index.ts
@@ -0,0 +1,4 @@
+export {
+ getDefaultSort as tableGetDefaultSort,
+ getSorted as tableGetSorted,
+} from './table.functions'
diff --git a/src-ts/lib/table/table-functions/table.functions.ts b/src-ts/lib/table/table-functions/table.functions.ts
new file mode 100644
index 000000000..cfd4d25af
--- /dev/null
+++ b/src-ts/lib/table/table-functions/table.functions.ts
@@ -0,0 +1,63 @@
+import { Sort } from '../../pagination'
+import { TableColumn } from '../table-column.model'
+
+export function getDefaultSort(columns: ReadonlyArray>): Sort | undefined {
+
+ const defaultSortColumn: TableColumn | undefined = columns.find(col => col.isDefaultSort)
+ || columns.find(col => !!col.propertyName)
+
+ const defaultSort: Sort | undefined = !defaultSortColumn?.propertyName
+ ? undefined
+ : {
+ direction: defaultSortColumn.defaultSortDirection || 'asc',
+ fieldName: defaultSortColumn.propertyName,
+ }
+
+ return defaultSort
+}
+
+export function getSorted(
+ data: ReadonlyArray,
+ cols: ReadonlyArray>,
+ sort?: Sort,
+): ReadonlyArray {
+
+ // get the sort column
+ const sortColumn: TableColumn | undefined = !!sort
+ ? cols.find(col => col.propertyName === sort.fieldName)
+ : undefined
+
+ const sortedData: Array = [...data]
+
+ // if we don't have a column to sort, don't sort
+ if (!sort || !sortColumn) {
+ return sortedData
+ }
+
+ function sortNumbers(a: number, b: number, direction: 'asc' | 'desc'): number {
+ return direction === 'asc' ? a - b : b - a
+ }
+
+ if (sortColumn.type === 'money') {
+ return sortedData
+ .sort((a: T, b: T) => sortNumbers(+a[sort.fieldName], +b[sort.fieldName], sort.direction))
+ }
+
+ if (sortColumn.type === 'date') {
+ return sortedData
+ .sort((a: T, b: T) => sortNumbers(
+ (a[sort.fieldName] as Date).getTime(),
+ (b[sort.fieldName] as Date).getTime(),
+ sort.direction
+ ))
+ }
+
+ return sortedData
+ .sort((a: T, b: T) => {
+ const aField: string = a[sort.fieldName]
+ const bField: string = b[sort.fieldName]
+ return sort.direction === 'asc'
+ ? aField.localeCompare(bField)
+ : bField.localeCompare(aField)
+ })
+}
diff --git a/src-ts/lib/table/table-sort/TableSort.tsx b/src-ts/lib/table/table-sort/TableSort.tsx
new file mode 100644
index 000000000..2e1c189a6
--- /dev/null
+++ b/src-ts/lib/table/table-sort/TableSort.tsx
@@ -0,0 +1,38 @@
+import { FC, SVGProps } from 'react'
+
+import { Button } from '../../button'
+import { Sort } from '../../pagination'
+import { IconOutline } from '../../svgs'
+
+interface TableSortProps {
+ iconClass: string
+ isCurrentlySorted: boolean
+ propertyName?: string
+ sort?: Sort
+ toggleSort: (fieldName: string) => void
+}
+
+const TableSort: FC = (props: TableSortProps) => {
+
+ if (!props.propertyName || !props.sort) {
+ return <>>
+ }
+
+ // if this isn't the currently sorted field,
+ // use the disambiguated icon
+ const icon: FC