Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The following properties use the UniNav for the MVP:

### Navigation Types

The UniNav comprises 3 parts, `'marketing' | 'tool' | 'footer'`, which are described here.
The UniNav comprises 4 variants, `'marketing' | 'tool' | 'topgear' | 'footer'`, which are described here.

#### Marketing Navigation

Expand All @@ -55,6 +55,21 @@ This navigation is rendered for all non-marketing web properites (e.g https://pl

The navigation configuration is supplied upon initialization by the tool itself. See [2. Initialize tcUnNav](#2-initialize-tcuninav) for more info.

#### Topgear Navigation

Tool navigation automatically uses the Topgear header on exactly
`topgear.topcoder.com` and `topgear.topcoder-dev.com`. Integrations can also
request `type: 'topgear'`; it falls back to the tool header on any other host.
Marketing and footer navigation are unaffected.

The responsive header reuses community-app's Wipro, Topcoder, and Topgear logos.
Home links to `https://topgear-app.wipro.com`; Challenges links to
`/opportunities/challenge` on the current Topgear origin. It retains account
controls, without the tool selector, Leaderboard, or the community hero banner.
Deploy the navigation bundle before the Platform UI migration. Its CloudFront
CORS policy must allow the matching Topgear origin (included in the IAC template).
This change does not redirect production community-app URLs.

#### Footer Navigation

This navigation is rendered for all web properties, regardless if they're marketing sites or tools.
Expand Down Expand Up @@ -292,7 +307,7 @@ Information regarding the changes will be disseminated to the owners of all prop
| config.toolName | string | yes (tool nav only) | The name of the tool as it should appear in the header | none |
| config.toolRoot | string | yes (tool nav only) | The route to the tool as it should appear in the header | none |
| config.fullFooter | boolean | no | Shows the full footer navigation when it is true | false |
| config.type | Enum: 'marketing' \| 'tool' \| 'footer' | yes | The type of navigation to render | |
| config.type | Enum: 'marketing' \| 'tool' \| 'topgear' \| 'footer' | yes | The type of navigation to render | |
| config.user | 'auto' | no | Instruct the uni-nav to auto-fetch user profile info based on the `tcjwt` cookie value | {} |
| config.user | {photoURL, userId, initials, handle} | no | The logged in user | {} |
| config.supportMeta | {challengeId, isSelfService} | no | Additional meta data to be sent along with any support ticket created by the user | |
Expand Down
3 changes: 3 additions & 0 deletions iac/uni-nav.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ Resources:
# Platform UI Prod & Dev
- !Sub https://platform-ui.${Domain}

# Topgear community on Platform UI
- !Sub https://topgear.${Domain}

# Platform UI sub apps for Prod & Dev
- !Sub https://academy.${Domain}
- !Sub https://work.${Domain}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"build:nudge": "APP_BUILD_TARGET=nudge vite build --mode=production",
"build:base": "vite build --mode=production && npx rimraf ./types && npm run types",
"build": "npm run build:base && npm run build:nudge",
"lint": "npm run check",
"test": "node --experimental-strip-types --test tests/*.test.mjs",
"check": "svelte-check --tsconfig ./tsconfig.json",
"demo:dist": "npx http-server --cors --host local.topcoder-dev.com --port 8083 ./dist",
"demo:marketing": "npx http-server --host local.topcoder-dev.com --port 8081 ./demo/marketing -P http://local.topcoder-dev.com:8081? -o /",
Expand Down
6 changes: 6 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,9 @@ Both the types commit and the version patch commit will be authored by the user
For non-master branches, there is a build job in the CI that will check to see if there are any updates to the types required and exit with an error code 1 with message `WARNING: Types need updating`.

The error will not prevent merging a PR or deploying to an environment but will just be a flag for the PR owner and reviewers.

## Validation

Run `npm run lint` (Svelte/TypeScript diagnostics), `npm test` (hostname
selection regressions), and `npm run build` before proposing a change.
Tests use the Node version in `.nvmrc` and its TypeScript stripping support.
5 changes: 4 additions & 1 deletion src/lib/components/user-area/UserArea.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import Completedness from './Completedness.svelte';
import Button from '../Button.svelte';

/** Whether to offer the cross-application tool selector; hidden in community headers. */
export let showToolSelector = true;

const ctx = getAppContext();

// debounce updates to user if user.handle stays the same
Expand Down Expand Up @@ -98,7 +101,7 @@
<Button variant="primary-link" label="Login" onClick={onSignIn} />
</div>
{:else }
<ToolSelector />
{#if showToolSelector}<ToolSelector />{/if}
<UserAvatar
user={user}
onSignOut={onSignOut}
Expand Down
6 changes: 3 additions & 3 deletions src/lib/nudge-app/components/Animation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import styles from './Animation.module.scss';

export let animation: string;
export let cover: string;
export let cover: string | undefined;

let ref: Element | undefined = undefined;
let coverRef: HTMLDivElement | undefined = undefined;
Expand All @@ -14,7 +14,7 @@
dispatch('loaded', {animation: animation === true});
}

const loadAnimation = (path) => {
const loadAnimation = (path: string) => {
var animData = {
container: ref,
renderer: 'svg',
Expand All @@ -25,7 +25,7 @@
progressiveLoad: true
},
};
const bmAnim = window['bodymovin'].loadAnimation(animData);
const bmAnim = (window as any).bodymovin.loadAnimation(animData);
bmAnim.addEventListener('data_ready', () => {
if (coverRef) {
Object.assign(coverRef.style, {display: 'none'});
Expand Down
77 changes: 77 additions & 0 deletions src/lib/topgear-navigation/TopgearNavigation.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
@use 'lib/styles/fonts.scss' as *;

.header {
display: flex;
align-items: center;
gap: 32px;
min-height: 110px;
padding: 20px 40px;
background: #fff;
color: #00a2e0;
font-family: $roboto;
}

.branding {
display: flex;
align-items: center;
gap: 30px;

a { display: flex; flex-shrink: 0; }
}

.wipro { width: 90px; }
.topcoder { width: 135px; }
.topgear { width: 132px; }

.header img { height: auto; }

.links {
display: flex;
align-items: center;
gap: 40px;
margin-right: auto;
font-size: 18px;
font-weight: 700;

a {
position: relative;
padding: 12px 0;
}

a[aria-current='page']::after {
content: '';
position: absolute;
bottom: 0;
left: calc(50% - 4px);
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
}

.header a:focus-visible {
outline: 2px solid #16679a;
outline-offset: 4px;
}

.user { flex-shrink: 0; }

@media (max-width: 1023px) {
.header { gap: 24px; padding: 16px; }
.branding { gap: 16px; }
.links { gap: 24px; }
.wipro { width: 70px; }
.topcoder { width: 110px; }
.topgear { width: 100px; }
}

@media (max-width: 767px) {
.header { flex-wrap: wrap; gap: 16px; }
.branding { flex: 1 0 calc(100% - 106px); gap: 12px; }
.wipro { width: 64px; }
.topcoder { width: 100px; }
.topgear { order: 1; width: 90px; }
.links { order: 2; font-size: 16px; }
.user { order: 3; }
}
34 changes: 34 additions & 0 deletions src/lib/topgear-navigation/TopgearNavigation.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts">
import { onMount } from 'svelte';
import { checkAndLoadFonts } from 'lib/utils/fonts';
import { pathname } from 'lib/utils/location.store';
import UserArea from 'lib/components/user-area/UserArea.svelte';
import wiproLogo from './assets/wipro.png';
import topcoderLogo from './assets/topcoder.svg';
import topgearLogo from './assets/topgear.png';
import styles from './TopgearNavigation.module.scss';

const challengesUrl = `${window.location.origin}/opportunities/challenge`;
$: challengesActive = $pathname?.startsWith('/opportunities') || $pathname === '/challenges';

onMount(checkAndLoadFonts);
</script>

<header class={styles.header}>
<div class={styles.branding}>
<a href="https://www.wipro.com/" aria-label="Wipro home">
<img class={styles.wipro} src={wiproLogo} alt="Wipro" />
</a>
<a href="https://www.topcoder.com/" aria-label="Topcoder home">
<img class={styles.topcoder} src={topcoderLogo} alt="Topcoder" />
</a>
</div>
<nav aria-label="Topgear" class={styles.links}>
<a href="https://topgear-app.wipro.com">Home</a>
<a href={challengesUrl} aria-current={challengesActive ? 'page' : undefined}>Challenges</a>
</nav>
<div class={styles.user}>
<UserArea showToolSelector={false} />
</div>
<img class={styles.topgear} src={topgearLogo} alt="Topgear" />
</header>
29 changes: 29 additions & 0 deletions src/lib/topgear-navigation/assets/topcoder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/lib/topgear-navigation/assets/topgear.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/lib/topgear-navigation/assets/wipro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/lib/utils/navigation-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { NavigationType } from '../../main'

/**
* Selects the branded header only on the two Topgear community hosts.
* Tool integrations opt in automatically; explicit `topgear` requests fall back
* to the standard tool header elsewhere. Marketing and footer are unchanged.
* @param type Requested navigation variant.
* @param hostname Hostname without a port, normally window.location.hostname.
* @returns The navigation variant to load. Does not throw.
*/
export function resolveNavigationType(type: NavigationType, hostname: string): NavigationType {
const topgear = ['topgear.topcoder.com', 'topgear.topcoder-dev.com'].includes(hostname.toLowerCase());
return type === 'tool' || type === 'topgear' ? (topgear ? 'topgear' : 'tool') : type;
}
6 changes: 5 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { writable } from 'svelte/store'
import type { Writable } from 'svelte/store'

import { buildContext, type AuthUser, type NavigationHandler, type SupportMeta } from './lib/app-context'
import { resolveNavigationType } from './lib/utils/navigation-type'
import { loadNudgeApp } from './lib/functions/load-nudge-app'
import { PubSub } from './lib/utils/pubsub';
import { initializeUtmCookieHandler } from './lib/functions/utm-cookies.handler';
Expand All @@ -15,6 +16,7 @@ export type NavigationType = (
| 'footer'
| 'marketing'
| 'tool'
| 'topgear'
)

export type NavigationAppProps = {
Expand Down Expand Up @@ -52,6 +54,7 @@ const NavigationLoadersMap: Record<NavigationType, () => Promise<any>> = {
marketing: () => import('./lib/marketing-navigation/MarketingNavigation.svelte').then(d => d.default),
footer: () => import('./lib/footer-navigation/FooterNavigation.svelte').then(d => d.default),
tool: () => import('./lib/tool-navigation/ToolNavigation.svelte').then(d => d.default),
topgear: () => import('./lib/topgear-navigation/TopgearNavigation.svelte').then(d => d.default),
}

const instancesContextStore: { [key: string]: Map<string, Writable<any>> } = {}
Expand Down Expand Up @@ -102,10 +105,11 @@ async function init(
toolRoot,
handleNavigation,
supportMeta,
type: navType = 'tool',
type: requestedType = 'tool',
...navProps
} = props

const navType = resolveNavigationType(requestedType, window.location.hostname);
const loadNavigationFn = NavigationLoadersMap[navType];

if (!loadNavigationFn) {
Expand Down
Loading
Loading