Skip to content

Commit 16cc7bd

Browse files
committed
Universal Navigation
1 parent 07f5ed3 commit 16cc7bd

File tree

12 files changed

+183
-89
lines changed

12 files changed

+183
-89
lines changed

config/constants/development.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,8 @@ module.exports = {
4646
IDLE_TIMEOUT_MINUTES: 10,
4747
// duration to show the prompt saying user will be logged out, before actually logging out the user
4848
IDLE_TIMEOUT_GRACE_MINUTES: 5,
49-
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07'
49+
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07',
50+
UNIVERSAL_NAV_URL: '//uni-nav.topcoder-dev.com/v1/tc-universal-nav.js',
51+
HEADER_AUTH_URLS_HREF: `https://accounts-auth0.${DOMAIN}?utm_source=community-app-main`,
52+
HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`
5053
}

config/constants/production.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,8 @@ module.exports = {
4444
FILE_PICKER_CNAME: 'fs.topcoder.com',
4545
IDLE_TIMEOUT_MINUTES: 10,
4646
IDLE_TIMEOUT_GRACE_MINUTES: 5,
47-
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07'
47+
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07',
48+
UNIVERSAL_NAV_URL: '//uni-nav.topcoder.com/v1/tc-universal-nav.js',
49+
HEADER_AUTH_URLS_HREF: `https://accounts-auth0.${DOMAIN}?utm_source=community-app-main`,
50+
HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`
4851
}

src/components/App/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import React from 'react'
55
import TwoRowsLayout from '../TwoRowsLayout'
66

7-
const App = (content, topbar, sidebar) => () => {
7+
const App = (content, topbar, sidebar, footer) => () => {
88
return (
99
<TwoRowsLayout scrollIndependent>
1010
<TwoRowsLayout.Content>
1111
{topbar || null}
1212
{sidebar}
1313
{content}
14+
{footer || null}
1415
</TwoRowsLayout.Content>
1516
</TwoRowsLayout>
1617
)

src/components/TopBar/Topbar.module.scss

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/components/TopBar/index.js

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,77 @@
1-
/**
2-
* Component to render top bar of app
3-
*/
4-
import React from 'react'
1+
/* global tcUniNav */
2+
import React, { useRef, useState, useEffect } from 'react'
53
import PropTypes from 'prop-types'
6-
import cn from 'classnames'
7-
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
8-
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
9-
import { get } from 'lodash'
10-
import styles from './Topbar.module.scss'
11-
import Handle from '../Handle'
12-
import TopcoderLogo from '../../assets/images/topcoder-logo.png'
13-
import { COMMUNITY_APP_URL } from '../../config/constants'
14-
15-
const TopBar = ({ user, hideBottomLine }) => {
16-
return (
17-
<div
18-
className={cn(styles.topbar, { [styles['hide-line']]: hideBottomLine })}
19-
>
20-
<img src={TopcoderLogo} className={styles.logo} />
21-
{user && (
22-
<div className={styles.details}>
23-
Welcome,{' '}
24-
<Handle
25-
handle={user.handle}
26-
rating={get(user, 'maxRating.rating', 0)}
27-
/>
28-
<a href={`${COMMUNITY_APP_URL}/logout`}>
29-
<FontAwesomeIcon icon={faSignInAlt} className={styles.icon} />
30-
</a>
31-
</div>
32-
)}
33-
</div>
34-
)
4+
import { getInitials } from '../../util/url'
5+
import { COMMUNITY_APP_URL, HEADER_AUTH_URLS_HREF, HEADER_AUTH_URLS_LOCATION } from '../../config/constants'
6+
import _ from 'lodash'
7+
8+
let uniqueId = 0
9+
10+
const HEADER_AUTH_URLS = {
11+
href: HEADER_AUTH_URLS_HREF,
12+
location: HEADER_AUTH_URLS_LOCATION
13+
}
14+
const BASE = COMMUNITY_APP_URL
15+
16+
const TopBar = ({ auth }) => {
17+
const uniNavInitialized = useRef(false)
18+
const toolNameRef = useRef('Work Manager')
19+
const user = _.get(auth, 'user') || {}
20+
const authToken = _.get(auth, 'token')
21+
const isAuthenticated = !!authToken
22+
const authURLs = HEADER_AUTH_URLS
23+
const headerRef = useRef()
24+
const [headerId, setHeaderId] = useState(0)
25+
26+
const navigationUserInfo = {
27+
...user,
28+
initials: getInitials(user.firstName, user.lastName)
29+
}
30+
31+
useEffect(() => {
32+
uniqueId += 1
33+
setHeaderId(uniqueId)
34+
}, [])
35+
36+
useEffect(() => {
37+
if (uniNavInitialized.current || !headerId) {
38+
return
39+
}
40+
41+
uniNavInitialized.current = true
42+
43+
const regSource = window.location.pathname.split('/')[1]
44+
const retUrl = encodeURIComponent(window.location.href)
45+
tcUniNav('init', `headerNav-${headerId}`, {
46+
type: 'tool',
47+
toolName: toolNameRef.current,
48+
toolRoot: '/',
49+
user: isAuthenticated ? navigationUserInfo : null,
50+
signOut: () => {
51+
window.location = `${BASE}/logout?ref=nav`
52+
},
53+
signIn: () => {
54+
window.location = `${authURLs.location
55+
.replace('%S', retUrl)
56+
.replace('member?', '#!/member?')}&regSource=${regSource}`
57+
},
58+
signUp: () => {
59+
window.location = `${authURLs.location
60+
.replace('%S', retUrl)
61+
.replace('member?', '#!/member?')}&mode=signUp&regSource=${regSource}`
62+
}
63+
})
64+
}, [headerId])
65+
66+
return <div id={`headerNav-${headerId}`} ref={headerRef} />
67+
}
68+
69+
TopBar.defaultProps = {
70+
auth: {}
3571
}
3672

3773
TopBar.propTypes = {
38-
user: PropTypes.object,
39-
hideBottomLine: PropTypes.bool
74+
auth: PropTypes.shape()
4075
}
4176

4277
export default TopBar
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* global tcUniNav */
2+
import React, { useEffect, useRef, useState } from 'react'
3+
4+
let uniqueId = 0
5+
6+
export default function TopcoderFooter () {
7+
const footerRef = useRef()
8+
const footerInitialized = useRef(false)
9+
const [footerId, setFooterId] = useState(0)
10+
11+
useEffect(() => {
12+
uniqueId += 1
13+
setFooterId(uniqueId)
14+
}, [])
15+
16+
useEffect(() => {
17+
if (footerInitialized.current || !footerId) {
18+
return
19+
}
20+
21+
footerInitialized.current = true
22+
23+
tcUniNav('init', `footerNav-${footerId}`, {
24+
type: 'footer'
25+
})
26+
}, [footerId])
27+
28+
return <div id={`footerNav-${footerId}`} ref={footerRef} />
29+
}

src/config/constants.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export const {
2020
CHALLENGE_TYPE_ID,
2121
MARATHON_TYPE_ID,
2222
SEGMENT_API_KEY,
23-
MULTI_ROUND_CHALLENGE_TEMPLATE_ID
23+
MULTI_ROUND_CHALLENGE_TEMPLATE_ID,
24+
UNIVERSAL_NAV_URL,
25+
HEADER_AUTH_URLS_HREF,
26+
HEADER_AUTH_URLS_LOCATION
2427
} = process.env
2528
export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS === 'string' ? process.env.CREATE_FORUM_TYPE_IDS.split(',') : process.env.CREATE_FORUM_TYPE_IDS
2629

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Container to provide user info to TopBar component
3+
*/
4+
import React, { Component } from 'react'
5+
import TopcoderFooter from '../../components/TopcoderFooter'
6+
7+
class FooterContainer extends Component {
8+
render () {
9+
return <TopcoderFooter />
10+
}
11+
}
12+
13+
FooterContainer.propTypes = {
14+
}
15+
16+
export default FooterContainer

src/containers/TopbarContainer/index.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,13 @@ class TopbarContainer extends Component {
2121
}
2222

2323
render () {
24-
const { match } = this.props
25-
const isChalengeViewPage = match.path === '/projects/:projectId/challenges/:challengeId'
26-
const { user } = this.props.auth
27-
return <TopBar user={user} hideBottomLine={isChalengeViewPage} />
24+
return <TopBar auth={this.props.auth} />
2825
}
2926
}
3027

3128
TopbarContainer.propTypes = {
3229
loadUser: PropTypes.func.isRequired,
3330
setActiveProject: PropTypes.func.isRequired,
34-
match: PropTypes.any.isRequired,
3531
auth: PropTypes.object.isRequired,
3632
activeProjectId: PropTypes.number,
3733
projectId: PropTypes.string

src/index.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom'
66
import './styles/main.scss'
77
import 'react-redux-toastr/lib/css/react-redux-toastr.min.css'
88
import App from './App'
9-
import { SEGMENT_API_KEY } from './config/constants'
9+
import { SEGMENT_API_KEY, UNIVERSAL_NAV_URL } from './config/constants'
1010

1111
ReactDOM.render(<App />, document.getElementById('root'))
1212

@@ -18,3 +18,29 @@ if (!_.isEmpty(SEGMENT_API_KEY)) {
1818
}}();
1919
}
2020
/* eslint-enable */
21+
22+
// <!-- Start of topcoder Topcoder Universal Navigation script -->
23+
// eslint-disable-next-line no-unused-expressions
24+
!(function (n, t, e, a, c, i, o) {
25+
// eslint-disable-next-line no-unused-expressions, no-sequences
26+
;(n['TcUnivNavConfig'] = c),
27+
(n[c] =
28+
n[c] ||
29+
function () {
30+
;(n[c].q = n[c].q || []).push(arguments)
31+
}),
32+
(n[c].l = 1 * new Date())
33+
// eslint-disable-next-line no-unused-expressions, no-sequences
34+
;(i = t.createElement(e)), (o = t.getElementsByTagName(e)[0])
35+
i.async = 1
36+
i.type = 'module'
37+
i.src = a
38+
o.parentNode.insertBefore(i, o)
39+
})(
40+
window,
41+
document,
42+
'script',
43+
UNIVERSAL_NAV_URL,
44+
'tcUniNav'
45+
)
46+
// <!-- End of topcoder Topcoder Universal Navigation script -->

src/routes.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import _ from 'lodash'
88
import { BETA_MODE_COOKIE_TAG } from './config/constants'
99
import renderApp from './components/App'
1010
import TopBarContainer from './containers/TopbarContainer'
11+
import FooterContainer from './containers/FooterContainer'
1112
import Tab from './containers/Tab'
1213
import Challenges from './containers/Challenges'
1314
import ChallengeEditor from './containers/ChallengeEditor'
@@ -160,7 +161,8 @@ class Routes extends React.Component {
160161
render={() => renderApp(
161162
<Challenges menu='NULL' warnMessage={'You are not authorized to use this application'} />,
162163
<TopBarContainer />,
163-
<Tab />
164+
<Tab />,
165+
<FooterContainer />
164166
)()}
165167
/>
166168
<Redirect to='/' />
@@ -170,14 +172,16 @@ class Routes extends React.Component {
170172
render={() => renderApp(
171173
<Challenges dashboard key='dashboard' />,
172174
<TopBarContainer />,
173-
<Tab />
175+
<Tab />,
176+
<FooterContainer />
174177
)()}
175178
/>
176179
<Route exact path='/projects'
177180
render={() => renderApp(
178181
<Challenges menu='NULL' key='projects' />,
179182
<TopBarContainer />,
180-
<Tab />
183+
<Tab />,
184+
<FooterContainer />
181185
)()}
182186
/>
183187
{
@@ -186,7 +190,8 @@ class Routes extends React.Component {
186190
render={() => renderApp(
187191
<Users />,
188192
<TopBarContainer />,
189-
<Tab />
193+
<Tab />,
194+
<FooterContainer />
190195
)()}
191196
/>
192197
)
@@ -195,7 +200,8 @@ class Routes extends React.Component {
195200
render={() => renderApp(
196201
<Challenges selfService />,
197202
<TopBarContainer />,
198-
<Tab selfService />
203+
<Tab selfService />,
204+
<FooterContainer />
199205
)()}
200206
/>
201207
{
@@ -204,7 +210,8 @@ class Routes extends React.Component {
204210
render={({ match }) => renderApp(
205211
<ChallengeEditor />,
206212
<TopBarContainer />,
207-
<Tab projectId={match.params.projectId} menu={'New Challenge'} />
213+
<Tab projectId={match.params.projectId} menu={'New Challenge'} />,
214+
<FooterContainer />
208215
)()} />
209216
)
210217
}
@@ -214,13 +221,15 @@ class Routes extends React.Component {
214221
render={({ match }) => renderApp(
215222
<ChallengeEditor />,
216223
<TopBarContainer />,
217-
<Tab projectId={match.params.projectId} menu={'New Challenge'} />
224+
<Tab projectId={match.params.projectId} menu={'New Challenge'} />,
225+
<FooterContainer />
218226
)()} />
219227
<Route exact path='/projects/:projectId/challenges'
220228
render={({ match }) => renderApp(
221229
<Challenges projectId={match.params.projectId} key='challenges' />,
222230
<TopBarContainer projectId={match.params.projectId} />,
223-
<Tab projectId={match.params.projectId} />
231+
<Tab projectId={match.params.projectId} />,
232+
<FooterContainer />
224233
)()} />
225234
{/* If path is not defined redirect to landing page */}
226235
<Redirect to='/' />

src/util/url.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Get initials from user profile
3+
* @param {String} firstName first name
4+
* @param {String} lastName last name
5+
* @returns {String}
6+
*/
7+
export function getInitials (firstName = '', lastName = '') {
8+
return `${firstName.slice(0, 1)}${lastName.slice(0, 1)}`
9+
}

0 commit comments

Comments
 (0)