Skip to content

Commit 99cbe32

Browse files
authored
Merge pull request #953 from topcoder-platform/TSJR-314_skill-manager_landing-page
TSJR-57 skills manager -> dev
2 parents 0ef2d21 + ed11e65 commit 99cbe32

File tree

103 files changed

+3324
-65
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+3324
-65
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ workflows:
257257
branches:
258258
only:
259259
- dev
260+
- TSJR-314_skill-manager_landing-page
260261

261262
- deployQa:
262263
context: org-global

.vscode/components.code-snippets

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// ],
1616
// "description": "Log output to console"
1717
// }
18-
"[MFE] React component": {
18+
"[PLAT] React component": {
1919
"scope": "typescript,typescriptreact",
2020
"prefix": "rfc",
2121
"body": [
@@ -28,18 +28,39 @@
2828
"",
2929
"const ${1:ComponentName}: FC<${1:ComponentName}Props> = props => {",
3030
"",
31-
" return (",
31+
" return (",
32+
" <div className={styles.wrap}>",
33+
" </div>",
34+
" )",
35+
"}",
36+
"",
37+
"export default ${1:ComponentName}",
38+
""
39+
],
40+
"description": "Create a react functional component"
41+
},
42+
"[PLAT] Simple React component": {
43+
"scope": "typescript,typescriptreact",
44+
"prefix": "rfc",
45+
"body": [
46+
"import { FC } from 'react'",
47+
"",
48+
"import styles from './${1:ComponentName}.module.scss'",
49+
"",
50+
"interface ${1:ComponentName}Props {",
51+
"}",
52+
"",
53+
"const ${1:ComponentName}: FC<${1:ComponentName}Props> = props => (",
3254
" <div className={styles.wrap}>",
3355
" </div>",
34-
" )",
35-
"}",
56+
")",
3657
"",
3758
"export default ${1:ComponentName}",
3859
""
3960
],
4061
"description": "Create a react functional component"
4162
},
42-
"[MFE] export comp": {
63+
"[PLAT] export comp": {
4364
"scope": "typescript,typescriptreact",
4465
"prefix": "exp",
4566
"body": [
@@ -48,14 +69,21 @@
4869
],
4970
"description": "Export module"
5071
},
51-
"[MFE] use state": {
72+
"[PLAT] use state": {
5273
"scope": "typescript,typescriptreact",
5374
"prefix": "usest",
5475
"body": [
5576
"const [$1, set$2]: [$3, Dispatch<SetStateAction<$3>>] = useState($4)$0",
5677
]
5778
},
58-
"[MFE] Storybook Template": {
79+
"[PLAT] includes": {
80+
"scope": "css,scss",
81+
"prefix": "includes",
82+
"body": [
83+
"@import '@libs/ui/styles/includes';",
84+
]
85+
},
86+
"[PLAT] Storybook Template": {
5987
"scope": "typescript,typescriptreact",
6088
"prefix": "sb",
6189
"body": [

src/apps/admin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Admin App

src/apps/admin/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './src'

src/apps/admin/src/AdminApp.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { FC, useContext } from 'react'
2+
import { Outlet, Routes } from 'react-router-dom'
3+
4+
import { routerContext, RouterContextData } from '~/libs/core'
5+
import { SharedSwrConfig } from '~/libs/shared'
6+
7+
import { toolTitle } from './admin.routes'
8+
9+
const AdminApp: FC<{}> = () => {
10+
const { getChildRoutes }: RouterContextData = useContext(routerContext)
11+
12+
return (
13+
<SharedSwrConfig>
14+
<Outlet />
15+
<Routes>
16+
{getChildRoutes(toolTitle)}
17+
</Routes>
18+
</SharedSwrConfig>
19+
)
20+
}
21+
22+
export default AdminApp

src/apps/admin/src/admin.routes.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Navigate } from 'react-router-dom'
2+
3+
import { lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core'
4+
import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config'
5+
6+
import { skillsManagerRootRoute, skillsManagerRoutes } from './skills-manager'
7+
8+
const AdminApp: LazyLoadedComponent = lazyLoad(() => import('./AdminApp'))
9+
10+
export const rootRoute: string = (
11+
EnvironmentConfig.SUBDOMAIN === AppSubdomain.admin ? '' : `/${AppSubdomain.admin}`
12+
)
13+
14+
export const toolTitle: string = ToolTitle.admin
15+
export const absoluteRootRoute: string = `${window.location.origin}${rootRoute}`
16+
17+
export const adminRoutes: ReadonlyArray<PlatformRoute> = [
18+
{
19+
authRequired: true,
20+
children: [
21+
...skillsManagerRoutes,
22+
{
23+
element: <Navigate to={`${rootRoute}${skillsManagerRootRoute}`} />,
24+
id: 'Default Admin Route',
25+
route: '',
26+
},
27+
],
28+
domain: AppSubdomain.admin,
29+
element: <AdminApp />,
30+
id: toolTitle,
31+
rolesRequired: [UserRole.administrator],
32+
route: rootRoute,
33+
},
34+
]

src/apps/admin/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export {
2+
adminRoutes,
3+
rootRoute as adminRootRoute,
4+
} from './admin.routes'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { FC, useContext } from 'react'
2+
import { Outlet, Routes } from 'react-router-dom'
3+
4+
import { routerContext, RouterContextData } from '~/libs/core'
5+
6+
import { skillsManagerRoutes } from './skills-manager.routes'
7+
import { SkillsManagerContext } from './context'
8+
9+
const SkillsManager: FC<{}> = () => {
10+
const { getRouteElement }: RouterContextData = useContext(routerContext)
11+
12+
return (
13+
<SkillsManagerContext>
14+
<Outlet />
15+
<Routes>
16+
{skillsManagerRoutes.map(getRouteElement)}
17+
</Routes>
18+
</SkillsManagerContext>
19+
)
20+
}
21+
22+
export default SkillsManager
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import '@libs/ui/styles/includes';
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {
2+
Children,
3+
cloneElement,
4+
FC,
5+
isValidElement,
6+
ReactNode,
7+
useCallback,
8+
useEffect,
9+
useRef,
10+
useState,
11+
} from 'react'
12+
13+
import { AccordionItemProps } from './accordion-item'
14+
import styles from './Accordion.module.scss'
15+
16+
interface AccordionProps {
17+
children: JSX.Element[] | JSX.Element
18+
defaultOpen?: boolean
19+
}
20+
21+
function computeOpenSectionsState(props: AccordionProps): {[key: string]: boolean} {
22+
const newOpenState: {[key: string]: boolean} = {}
23+
24+
Children.forEach<ReactNode>(props.children, child => {
25+
if (!isValidElement(child)) {
26+
return
27+
}
28+
29+
const childKey = child.key as string
30+
newOpenState[childKey] = child.props.open ?? props.defaultOpen
31+
})
32+
33+
return newOpenState
34+
}
35+
36+
const Accordion: FC<AccordionProps> = props => {
37+
const prevProps = useRef({ ...props })
38+
const [openedSections, setOpenedSections] = useState<{[key: string]: boolean}>({})
39+
40+
const handleToggle = useCallback((key: string) => {
41+
setOpenedSections(all => ({ ...all, [key]: !all[key] }))
42+
}, [])
43+
44+
// check if props have changed and update the openedSections synchronously
45+
if (prevProps.current.defaultOpen !== props.defaultOpen) {
46+
prevProps.current = { ...props }
47+
Object.assign(openedSections, computeOpenSectionsState(props))
48+
}
49+
50+
// use an effect to make sure the changes are propagated in the state
51+
useEffect(() => {
52+
setOpenedSections(computeOpenSectionsState(props))
53+
// eslint-disable-next-line react-hooks/exhaustive-deps
54+
}, [props.defaultOpen])
55+
56+
const renderAccordions = (children: JSX.Element[] | JSX.Element): ReactNode => (
57+
Children.map<ReactNode, ReactNode>(children, child => {
58+
if (isValidElement(child)) {
59+
const childKey = child.key as string
60+
openedSections[childKey] = openedSections[childKey] ?? child.props.open ?? props.defaultOpen
61+
62+
return cloneElement(
63+
child,
64+
{
65+
open: !!openedSections[childKey],
66+
toggle: function toggle() { handleToggle(childKey) },
67+
} as AccordionItemProps,
68+
)
69+
}
70+
71+
return child
72+
})
73+
)
74+
75+
return (
76+
<div className={styles.wrap}>
77+
{renderAccordions(props.children)}
78+
</div>
79+
)
80+
}
81+
82+
export default Accordion
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@import '@libs/ui/styles/includes';
2+
3+
.wrap {
4+
+ .wrap {
5+
margin-top: $sp-8;
6+
}
7+
}
8+
9+
.itemHeader {
10+
display: flex;
11+
align-items: center;
12+
}
13+
14+
.icon {
15+
color: $turq-120;
16+
transition: 0.2s ease-in-out;
17+
margin-right: $sp-2;
18+
}
19+
20+
.menuIcon {
21+
color: $turq-120;
22+
}
23+
24+
.titleBar {
25+
display: flex;
26+
align-items: center;
27+
gap: $sp-2;
28+
}
29+
30+
.textLabel {
31+
font-family: $font-roboto;
32+
font-size: 20px;
33+
font-weight: $font-weight-medium;
34+
line-height: $sp-5;
35+
letter-spacing: 0.05px;
36+
}
37+
38+
.textLabel, .icon {
39+
cursor: pointer;
40+
}
41+
42+
.badge {
43+
display: block;
44+
font-family: $font-roboto;
45+
font-weight: $font-weight-bold;
46+
font-size: 14px;
47+
line-height: 22px;
48+
padding: 0 $sp-2;
49+
50+
color: $black-100;
51+
52+
background: $black-10;
53+
border-radius: $sp-4;
54+
}
55+
56+
.content {
57+
padding: $sp-4 $sp-6;
58+
}
59+
60+
.open {
61+
.icon {
62+
transform: rotateZ(180deg);
63+
}
64+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { FC, useMemo } from 'react'
2+
import classNames from 'classnames'
3+
4+
import { IconOutline } from '~/libs/ui'
5+
6+
import { ActionsMenu, ActionsMenuItem } from '../../actions-menu'
7+
8+
import styles from './AccordionItem.module.scss'
9+
10+
export interface AccordionItemProps {
11+
label?: string
12+
badgeCount?: number
13+
open?: boolean
14+
toggle?: () => void
15+
children: JSX.Element[] | JSX.Element | (() => JSX.Element[] | JSX.Element)
16+
menuActions: ActionsMenuItem[]
17+
onMenuAction: (a: string) => void
18+
}
19+
20+
const AccordionItem: FC<AccordionItemProps> = props => {
21+
const content = useMemo(() => (!props.open ? <></> : (
22+
<div className={styles.content}>
23+
{typeof props.children === 'function' ? props.children.call(undefined) : props.children}
24+
</div>
25+
)), [props.children, props.open])
26+
27+
return (
28+
<div className={classNames(styles.wrap, props.open && styles.open)}>
29+
<div className={styles.itemHeader}>
30+
<span className={styles.icon} onClick={props.toggle}>
31+
<IconOutline.ChevronDownIcon className='icon-lg' />
32+
</span>
33+
<div className={styles.titleBar}>
34+
{props.label && (
35+
<div className={styles.textLabel} onClick={props.toggle}>
36+
{props.label}
37+
</div>
38+
)}
39+
{props.badgeCount !== undefined && (
40+
<div className={styles.badge}>
41+
{props.badgeCount}
42+
</div>
43+
)}
44+
{props.menuActions?.length > 0 && (
45+
<ActionsMenu
46+
items={props.menuActions}
47+
onAction={props.onMenuAction}
48+
className={styles.menu}
49+
>
50+
<IconOutline.DotsVerticalIcon className={classNames('icon-lg', styles.menuIcon)} />
51+
</ActionsMenu>
52+
)}
53+
</div>
54+
</div>
55+
{content}
56+
</div>
57+
)
58+
}
59+
60+
export default AccordionItem
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as AccordionItem, type AccordionItemProps } from './AccordionItem'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as Accordion } from './Accordion'
2+
export * from './accordion-item'

0 commit comments

Comments
 (0)