Skip to content

Commit 77c94e7

Browse files
Merge branch 'dev' into feat/GAME-82
2 parents 1a92396 + 99844c2 commit 77c94e7

File tree

22 files changed

+524
-58
lines changed

22 files changed

+524
-58
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './input-image-picker'
22
export * from './form-input-autcomplete-option.enum'
33
export * from './input-rating'
4+
export * from './input-select'
45
export * from './input-text'
56
export * from './input-textarea'
67
export { inputOptional } from './input-wrapper'
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@import '../../../../styles/includes';
2+
3+
.selected {
4+
display: flex;
5+
align-items: center;
6+
margin-top: $space-xs;
7+
cursor: pointer;
8+
color: $black-100;
9+
10+
&-icon {
11+
margin-left: auto;
12+
padding: $border-xs 0;
13+
color: $turq-160;
14+
> svg {
15+
@include icon-size(14);
16+
}
17+
}
18+
}
19+
20+
.menu-wrap {
21+
position: absolute;
22+
top: calc(100% - 2px);
23+
left: 0;
24+
width: 100%;
25+
&:not(:empty) {
26+
z-index: 9;
27+
}
28+
}
29+
30+
.select-menu {
31+
position: absolute;
32+
top: 100%;
33+
left: -1px;
34+
right: -1px;
35+
background: $tc-white;
36+
border: $border-xs solid $black-40;
37+
border-radius: 0 0 $space-xs $space-xs;
38+
padding: $space-sm 0;
39+
max-height: 230px;
40+
overflow: auto;
41+
}
42+
43+
.select-option {
44+
font-weight: normal;
45+
color: $black-100;
46+
padding: $space-sm $space-lg;
47+
48+
&:hover:global(:not(.selected)) {
49+
background: $turq-160;
50+
color: $tc-white;
51+
cursor: pointer;
52+
}
53+
54+
&:global(.selected) {
55+
font-weight: bold;
56+
}
57+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import classNames from 'classnames'
2+
import {
3+
ChangeEvent,
4+
Dispatch,
5+
FC,
6+
MutableRefObject,
7+
ReactNode,
8+
SetStateAction,
9+
useRef,
10+
useState,
11+
} from 'react'
12+
13+
import { useClickOutside } from '../../../../hooks'
14+
import { IconOutline } from '../../../../svgs'
15+
import { InputWrapper } from '../input-wrapper'
16+
17+
import styles from './InputSelect.module.scss'
18+
19+
export interface InputSelectOption {
20+
label?: ReactNode
21+
value: string
22+
}
23+
24+
interface InputSelectProps {
25+
readonly dirty?: boolean
26+
readonly disabled?: boolean
27+
readonly error?: string
28+
readonly hideInlineErrors?: boolean
29+
readonly hint?: string
30+
readonly label?: string
31+
readonly name: string
32+
readonly onChange: (event: ChangeEvent<HTMLInputElement>) => void
33+
readonly options: Array<InputSelectOption>
34+
readonly tabIndex?: number
35+
readonly value?: string
36+
}
37+
38+
const InputSelect: FC<InputSelectProps> = (props: InputSelectProps) => {
39+
const triggerRef: MutableRefObject<any> = useRef(undefined)
40+
const [menuIsVisible, setMenuIsVisible]: [boolean, Dispatch<SetStateAction<boolean>>] = useState(false)
41+
42+
const selectedOption: InputSelectOption | undefined = props.options.find(option => option.value === props.value)
43+
44+
const label: (option: InputSelectOption) => ReactNode = (option?: InputSelectOption) => (
45+
option ? option.label ?? option.value : ''
46+
)
47+
48+
const toggleMenu: () => void = () => setMenuIsVisible((wasVisible) => !wasVisible)
49+
50+
const select: (option: InputSelectOption) => () => void = (option: InputSelectOption) => () => {
51+
props.onChange({
52+
target: {value: option.value} ,
53+
} as unknown as ChangeEvent<HTMLInputElement>)
54+
toggleMenu()
55+
}
56+
57+
useClickOutside(triggerRef.current, () => setMenuIsVisible(false))
58+
59+
return (
60+
<InputWrapper
61+
{...props}
62+
dirty={!!props.dirty}
63+
disabled={!!props.disabled}
64+
hint={props.hint ?? ''}
65+
label={props.label ?? ''}
66+
type='text'
67+
className={styles['select-input-wrapper']}
68+
hideInlineErrors={props.hideInlineErrors}
69+
ref={triggerRef}
70+
>
71+
<div className={styles['selected']} onClick={toggleMenu}>
72+
<span className='body-small'>{selectedOption ? label(selectedOption) : ''}</span>
73+
<span className={styles['selected-icon']}>
74+
<IconOutline.ChevronDownIcon />
75+
</span>
76+
</div>
77+
78+
<div className={styles['menu-wrap']}>
79+
{menuIsVisible && (
80+
<div className={styles['select-menu']}>
81+
{props.options.map((option) => (
82+
<div
83+
className={
84+
classNames(
85+
styles['select-option'],
86+
'body-main',
87+
selectedOption === option && 'selected',
88+
)
89+
}
90+
onClick={select(option)}
91+
key={option.value}
92+
>
93+
{label(option)}
94+
</div>
95+
))}
96+
</div>
97+
)}
98+
</div>
99+
100+
</InputWrapper>
101+
)
102+
}
103+
104+
export default InputSelect
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as InputSelect } from './InputSelect'

src-ts/lib/form/form-groups/form-input/input-wrapper/InputWrapper.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ $error-line-height: 14px;
3030
box-sizing: border-box;
3131
border-radius: $space-xs;
3232
margin-bottom: calc($error-line-height + $space-xs);
33+
position: relative;
3334

3435
&.rating {
3536
border-color: transparent;

src-ts/lib/form/form-groups/form-input/input-wrapper/InputWrapper.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import classNames from 'classnames'
2-
import { Dispatch, FC, ReactNode, SetStateAction, useState } from 'react'
2+
import { Dispatch, forwardRef, ForwardRefExoticComponent, ReactNode, SetStateAction, useState } from 'react'
33

44
import { IconSolid } from '../../../../svgs'
55

@@ -16,18 +16,19 @@ interface InputWrapperProps {
1616
readonly hideInlineErrors?: boolean
1717
readonly hint?: string
1818
readonly label: string | JSX.Element
19-
readonly tabIndex: number
19+
readonly tabIndex?: number
2020
readonly type: 'checkbox' | 'password' | 'rating' | 'text' | 'textarea'
2121
}
2222

23-
const InputWrapper: FC<InputWrapperProps> = (props: InputWrapperProps) => {
23+
const InputWrapper: ForwardRefExoticComponent<InputWrapperProps> = forwardRef<HTMLDivElement, InputWrapperProps>((props: InputWrapperProps, ref) => {
2424

2525
const [focusStyle, setFocusStyle]: [string | undefined, Dispatch<SetStateAction<string | undefined>>] = useState<string | undefined>()
2626

2727
const isShowError: () => boolean = () => !!props.error && !!props.dirty && !props.hideInlineErrors
2828
const showError: boolean = isShowError()
2929
const formFieldClasses: string = classNames(
3030
styles.input,
31+
'input-el',
3132
styles[props.type],
3233
props.disabled ? styles.disabled : undefined,
3334
focusStyle,
@@ -43,8 +44,9 @@ const InputWrapper: FC<InputWrapperProps> = (props: InputWrapperProps) => {
4344

4445
return (
4546
<div
46-
className={classNames(styles['input-wrapper'], styles[props.type])}
47-
tabIndex={props.type === 'rating' ? props.tabIndex : -1}
47+
className={classNames(styles['input-wrapper'], 'input-wrapper', styles[props.type])}
48+
tabIndex={props.type === 'rating' ? (props.tabIndex ?? -1) : -1}
49+
ref={ref}
4850
>
4951

5052
<div
@@ -85,6 +87,6 @@ const InputWrapper: FC<InputWrapperProps> = (props: InputWrapperProps) => {
8587
)}
8688
</div>
8789
)
88-
}
90+
})
8991

9092
export default InputWrapper

src-ts/lib/form/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ export {
88
} from './form-functions'
99
export * from './form-input.model'
1010
export * from './form-group.model'
11-
export { inputOptional, FormInputAutocompleteOption } from './form-groups'
11+
export * from './form-groups/form-input'
1212
export * from './validator-functions'

src-ts/lib/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './use-check-is-mobile.hook'
22
export * from './use-click-outside.hook'
33
export * from './use-on-hover-element.hook'
4+
export * from './use-storage.hook'
45
export * from './use-window-size.hook'
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Dispatch, SetStateAction, useCallback, useState } from 'react'
2+
3+
type StorageTypes = 'localStorage' | 'sessionStorage'
4+
5+
export function useStorage<T>(
6+
storageType: StorageTypes,
7+
storageKey: string,
8+
initialValue?: T
9+
): [T, Dispatch<SetStateAction<T>>] {
10+
const storage: Storage = window[storageType]
11+
12+
const readStoredValue: () => T = useCallback(() => {
13+
try {
14+
// Get from local storage by key
15+
const item: string | null = storage.getItem(storageKey)
16+
// Parse stored json or if none return initialValue
17+
return item ? JSON.parse(item) : initialValue
18+
} catch (error) {
19+
// If error also return value
20+
return initialValue
21+
}
22+
}, [storage, storageKey, initialValue])
23+
24+
// State to store our value
25+
// Pass initial state function to useState so logic is only executed once
26+
const [storedValue, setStoredValue]: [T, Dispatch<SetStateAction<T>>] = useState(readStoredValue())
27+
28+
// Return a wrapped version of useState's setter function that
29+
// persists the new value to local or session storage.
30+
const setValue: Dispatch<SetStateAction<T>> = useCallback((value: T) => {
31+
try {
32+
// Allow value to be a function so we have same API as useState
33+
setStoredValue((storedv: T) => {
34+
const valueToStore: T = value instanceof Function ? value(storedv) : value
35+
36+
if (valueToStore === undefined) {
37+
storage.removeItem(storageKey)
38+
} else {
39+
// Save to local storage
40+
storage.setItem(storageKey, JSON.stringify(valueToStore))
41+
}
42+
43+
return valueToStore
44+
})
45+
} catch (error) {
46+
// A more advanced implementation would handle the error case
47+
// tslint:disable-next-line:no-console
48+
console.error(error)
49+
}
50+
}, [storage, storageKey]) as Dispatch<SetStateAction<T>>
51+
52+
return [storedValue, setValue]
53+
}
54+
55+
export const useLocalStorage: <T, >(
56+
key: string,
57+
initialValue?: T
58+
) => [T, Dispatch<SetStateAction<T>>] = <T>(
59+
key: string,
60+
initialValue?: T
61+
) => useStorage<T>('localStorage', key, initialValue)
62+
63+
export const useSessionStorage: <T, >(
64+
key: string,
65+
initialValue?: T
66+
) => [T, Dispatch<SetStateAction<T>>] = <T>(
67+
key: string,
68+
initialValue?: T
69+
) => useStorage<T>('sessionStorage', key, initialValue)

src-ts/tools/learn/course-certificate/certificate-view/certificate/Certificate.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import classNames from 'classnames'
22
import { FC, MutableRefObject } from 'react'
33

4+
import { LearnConfig } from '../../../learn-config'
45
import { LearnCertificateTrackType } from '../../../learn-lib'
56

67
import { CertificateBgPattern } from './certificate-bg-pattern'
@@ -13,18 +14,27 @@ import { ReactComponent as FccLogoSvg } from './vendor-fcc-logo.svg'
1314
interface CertificateProps {
1415
completedDate?: string
1516
course?: string
16-
elRef?: MutableRefObject<HTMLElement|any>
17+
elRef?: MutableRefObject<HTMLElement | any>
1718
provider?: string
1819
tcHandle?: string
1920
type?: LearnCertificateTrackType
2021
userName?: string
2122
}
2223

2324
const Certificate: FC<CertificateProps> = (props: CertificateProps) => {
25+
2426
const certificateType: LearnCertificateTrackType = props.type ?? 'DEV'
2527

28+
const elementSelector: { [attr: string]: string } = {
29+
[LearnConfig.CERT_ELEMENT_SELECTOR.attribute]: LearnConfig.CERT_ELEMENT_SELECTOR.value,
30+
}
31+
2632
return (
27-
<div className={styles['wrap']} ref={props.elRef}>
33+
<div
34+
{...elementSelector}
35+
className={styles['wrap']}
36+
ref={props.elRef}
37+
>
2838
<div className={classNames(styles['details'], `theme-${certificateType.toLowerCase()}`)}>
2939
<h2 className='details grad'>Topcoder Academy</h2>
3040
<h3>Certificate of Course Completion</h3>

0 commit comments

Comments
 (0)