From c5def7c7577881c081ac790a0e975355ba72864e Mon Sep 17 00:00:00 2001 From: jmgasper Date: Fri, 9 Oct 2026 16:22:37 +1100 Subject: [PATCH] PM-6555: Update app drawer menu with new Talent and Staff tools What was broken The app drawer (tool selector) was outdated. Members still saw removed items (Community Home, Practice, a Customer section whose "Meet our Talent" host now redirects to www), and staff only had Work Manager and Connect, which both redirect to the Work app, instead of the new staff tools. Styling and icons did not match the new design. Root cause The drawer config (tool-selector-nav-items.ts) and its ToolMenu component had not been updated for the new Figma design. The old layout also could not express the design's two-column grouping: it relied on a max-height wrap. What was changed - Talent: Earn (Opportunities, Engagement Portal, Copilot Portal, Review App, Wallet) and Compete in the left column; AI, Learn (Topcoder Academy, Articles, Archive) and Connect (Public Forums, Discord) in the right column. Community Home, Practice and the Customer section were removed. - Staff: Delivery & Payments (Work App, Wallet Admin), Sales & Customers (Customer Portal, Salesforce, Sales Pipeline, Procurement) and Insights (Analytics, Reports) in the left column; Community & Content (Support, Payload CMS, Mass Email, Campus) and Team & Platform (Leave Tracker, System Admin) in the right column. - New hosts in hosts.ts follow the .${TC_DOMAIN} pattern of the platform-ui apps. Payload CMS is cms./admin, Archive is archive. and Salesforce is topcoder.my.salesforce.com. - Updated descriptions to the design copy. Descriptions are hidden until a tool is hovered or keyboard focused, one at a time. - Sections now list explicit columns (section -> columns -> groups -> items), and ToolMenu renders them as flex columns that stack on mobile. - Restyled the drawer to the design: 560px card, Nunito Sans, TALENT/STAFF and group titles, 32px tinted icon tiles and 8px item spacing. - Replaced the drawer icons with tinted tiles using Material Symbols Rounded (Apache-2.0) and Tabler (MIT) glyphs, colored by group. Rebuilt sprite.svg, removed the icons of deleted items and documented the format in public/assets/tools/README.md. - ToolSelector hides the drawer trigger when no section matches the user's roles (eg. customer-only users), instead of opening an empty popup. Any added/updated tests - Added tests/tool-selector-nav-items.test.mjs. It loads the real drawer modules through Vite's SSR loader with prod env and a stubbed tcjwt cookie. It checks that staff see the designed Talent and Staff layout, that members only see Talent, and that customer-only users get no items. It also checks that every tool has a description, an icon present in sprite.svg and its expected production URL. - npm run lint (svelte-check), npm test and npm run build pass. Co-Authored-By: Claude Opus 5.5 --- public/assets/tools/README.md | 16 + public/assets/tools/ai-hub.svg | 28 +- public/assets/tools/analytics.svg | 1 + public/assets/tools/archive.svg | 1 + public/assets/tools/articles.svg | 2 +- public/assets/tools/campus.svg | 1 + public/assets/tools/challenges.svg | 2 +- public/assets/tools/connect.svg | 22 - public/assets/tools/copilot-portal.svg | 19 +- public/assets/tools/customer-portal.svg | 1 + public/assets/tools/discord.svg | 2 +- public/assets/tools/forums.svg | 2 +- public/assets/tools/gigs.svg | 2 +- public/assets/tools/leave-tracker.svg | 1 + public/assets/tools/mass-email.svg | 1 + public/assets/tools/mm-tournament.svg | 23 +- public/assets/tools/mm.svg | 2 +- public/assets/tools/payload-cms.svg | 1 + public/assets/tools/payments.svg | 2 +- public/assets/tools/practice.svg | 1 - public/assets/tools/procurement.svg | 1 + public/assets/tools/reports.svg | 1 + public/assets/tools/review.svg | 2 +- public/assets/tools/sales-pipeline.svg | 1 + public/assets/tools/salesforce.svg | 1 + public/assets/tools/sprite.svg | 400 ++++-------------- public/assets/tools/support.svg | 1 + public/assets/tools/system-admin.svg | 1 + public/assets/tools/talent-search.svg | 1 - public/assets/tools/tcacademy.svg | 2 +- public/assets/tools/topcrowd.svg | 1 - public/assets/tools/wallet-admin.svg | 1 + public/assets/tools/work-app.svg | 1 + public/assets/tools/work-manager.svg | 22 - .../tool-selector/ToolMenu.module.scss | 137 +++--- .../components/tool-selector/ToolMenu.svelte | 93 ++-- .../tool-selector/ToolSelector.svelte | 48 ++- src/lib/config/hosts.ts | 18 + .../config/nav-menu/all-nav-items.config.ts | 153 +++++-- .../nav-menu/tool-selector-nav-items.ts | 146 ++++--- tests/tool-selector-nav-items.test.mjs | 136 ++++++ types/src/lib/config/hosts.d.ts | 16 + .../nav-menu/tool-selector-nav-items.d.ts | 7 + 43 files changed, 639 insertions(+), 681 deletions(-) create mode 100644 public/assets/tools/analytics.svg create mode 100644 public/assets/tools/archive.svg create mode 100644 public/assets/tools/campus.svg delete mode 100644 public/assets/tools/connect.svg create mode 100644 public/assets/tools/customer-portal.svg create mode 100644 public/assets/tools/leave-tracker.svg create mode 100644 public/assets/tools/mass-email.svg create mode 100644 public/assets/tools/payload-cms.svg delete mode 100644 public/assets/tools/practice.svg create mode 100644 public/assets/tools/procurement.svg create mode 100644 public/assets/tools/reports.svg create mode 100644 public/assets/tools/sales-pipeline.svg create mode 100644 public/assets/tools/salesforce.svg create mode 100644 public/assets/tools/support.svg create mode 100644 public/assets/tools/system-admin.svg delete mode 100644 public/assets/tools/talent-search.svg delete mode 100644 public/assets/tools/topcrowd.svg create mode 100644 public/assets/tools/wallet-admin.svg create mode 100644 public/assets/tools/work-app.svg delete mode 100644 public/assets/tools/work-manager.svg create mode 100644 tests/tool-selector-nav-items.test.mjs diff --git a/public/assets/tools/README.md b/public/assets/tools/README.md index 73ce119a..c6e5a4c3 100644 --- a/public/assets/tools/README.md +++ b/public/assets/tools/README.md @@ -15,3 +15,19 @@ eg: ``` + +## App drawer icons +The app drawer icons are 32x32 tiles: a tinted rounded square (`rx="4"`) with a 24x24 glyph centered on it, colored per drawer group: + +| Groups | Tile | Glyph | +| --- | --- | --- | +| Earn, Delivery & Payments | `#DEFBE6` | `#198038` | +| Compete | `#FFF2E8` | `#BA4E00` | +| AI, Insights | `#FFE5EC` | `#C1294F` | +| Learn | `#E5F3FF` | `#2C5771` | +| Connect, Community & Content | `#F6F2FF` | `#8A3FFC` | +| Sales & Customers | `#FCF4D6` | `#8E6A00` | +| Team & Platform | `#D9FBFB` | `#007D79` | + +Glyphs come from [Material Symbols Rounded](https://github.com/google/material-design-icons) (weight 600, Apache-2.0), except `copilot-portal`, `mm`, `sales-pipeline` and `discord`, which use [Tabler Icons](https://tabler.io/icons) (MIT). +Each icon also exists as `./{file-name}.svg`; keep its `` in `./sprite.svg` in sync, and avoid inner `id`s so symbols don't clash inside the shared sprite. diff --git a/public/assets/tools/ai-hub.svg b/public/assets/tools/ai-hub.svg index d6b65143..00470065 100644 --- a/public/assets/tools/ai-hub.svg +++ b/public/assets/tools/ai-hub.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/public/assets/tools/analytics.svg b/public/assets/tools/analytics.svg new file mode 100644 index 00000000..b3d9cc51 --- /dev/null +++ b/public/assets/tools/analytics.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/archive.svg b/public/assets/tools/archive.svg new file mode 100644 index 00000000..a17d5f2e --- /dev/null +++ b/public/assets/tools/archive.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/articles.svg b/public/assets/tools/articles.svg index f38ae9e7..fb57fe1a 100644 --- a/public/assets/tools/articles.svg +++ b/public/assets/tools/articles.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/campus.svg b/public/assets/tools/campus.svg new file mode 100644 index 00000000..32ac314c --- /dev/null +++ b/public/assets/tools/campus.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/challenges.svg b/public/assets/tools/challenges.svg index 162bb4a6..2a55a448 100644 --- a/public/assets/tools/challenges.svg +++ b/public/assets/tools/challenges.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/connect.svg b/public/assets/tools/connect.svg deleted file mode 100644 index d82908d6..00000000 --- a/public/assets/tools/connect.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/assets/tools/copilot-portal.svg b/public/assets/tools/copilot-portal.svg index 421ec81b..1dee056e 100644 --- a/public/assets/tools/copilot-portal.svg +++ b/public/assets/tools/copilot-portal.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - - - - + diff --git a/public/assets/tools/customer-portal.svg b/public/assets/tools/customer-portal.svg new file mode 100644 index 00000000..7b23e4d9 --- /dev/null +++ b/public/assets/tools/customer-portal.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/discord.svg b/public/assets/tools/discord.svg index 4ceb835c..0ba6cb1e 100644 --- a/public/assets/tools/discord.svg +++ b/public/assets/tools/discord.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/forums.svg b/public/assets/tools/forums.svg index 2981f072..a9b20fe8 100644 --- a/public/assets/tools/forums.svg +++ b/public/assets/tools/forums.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/gigs.svg b/public/assets/tools/gigs.svg index ade9b2b6..347cba30 100644 --- a/public/assets/tools/gigs.svg +++ b/public/assets/tools/gigs.svg @@ -1 +1 @@ - + diff --git a/public/assets/tools/leave-tracker.svg b/public/assets/tools/leave-tracker.svg new file mode 100644 index 00000000..94a19187 --- /dev/null +++ b/public/assets/tools/leave-tracker.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/mass-email.svg b/public/assets/tools/mass-email.svg new file mode 100644 index 00000000..c7d64d80 --- /dev/null +++ b/public/assets/tools/mass-email.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/mm-tournament.svg b/public/assets/tools/mm-tournament.svg index 7eb48396..81b2b997 100644 --- a/public/assets/tools/mm-tournament.svg +++ b/public/assets/tools/mm-tournament.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - - - - - - - + diff --git a/public/assets/tools/mm.svg b/public/assets/tools/mm.svg index 1af6911d..7061701f 100644 --- a/public/assets/tools/mm.svg +++ b/public/assets/tools/mm.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/payload-cms.svg b/public/assets/tools/payload-cms.svg new file mode 100644 index 00000000..8f72cfff --- /dev/null +++ b/public/assets/tools/payload-cms.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/payments.svg b/public/assets/tools/payments.svg index fcdc23ba..76b7b130 100644 --- a/public/assets/tools/payments.svg +++ b/public/assets/tools/payments.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/practice.svg b/public/assets/tools/practice.svg deleted file mode 100644 index ec03a261..00000000 --- a/public/assets/tools/practice.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/assets/tools/procurement.svg b/public/assets/tools/procurement.svg new file mode 100644 index 00000000..0fff3b67 --- /dev/null +++ b/public/assets/tools/procurement.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/reports.svg b/public/assets/tools/reports.svg new file mode 100644 index 00000000..44ebc528 --- /dev/null +++ b/public/assets/tools/reports.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/review.svg b/public/assets/tools/review.svg index 21041f9a..1f2da4cc 100644 --- a/public/assets/tools/review.svg +++ b/public/assets/tools/review.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/sales-pipeline.svg b/public/assets/tools/sales-pipeline.svg new file mode 100644 index 00000000..f2c7285f --- /dev/null +++ b/public/assets/tools/sales-pipeline.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/salesforce.svg b/public/assets/tools/salesforce.svg new file mode 100644 index 00000000..40718a15 --- /dev/null +++ b/public/assets/tools/salesforce.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/sprite.svg b/public/assets/tools/sprite.svg index dcd72689..e8f0613d 100644 --- a/public/assets/tools/sprite.svg +++ b/public/assets/tools/sprite.svg @@ -1,104 +1,19 @@ - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -117,20 +32,8 @@ - - - - - - - - - - - - - - + + @@ -164,87 +67,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -261,109 +86,22 @@ - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -382,35 +120,71 @@ - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/tools/support.svg b/public/assets/tools/support.svg new file mode 100644 index 00000000..d3fe516f --- /dev/null +++ b/public/assets/tools/support.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/system-admin.svg b/public/assets/tools/system-admin.svg new file mode 100644 index 00000000..1b5712b5 --- /dev/null +++ b/public/assets/tools/system-admin.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/talent-search.svg b/public/assets/tools/talent-search.svg deleted file mode 100644 index 36706d43..00000000 --- a/public/assets/tools/talent-search.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/public/assets/tools/tcacademy.svg b/public/assets/tools/tcacademy.svg index f42595be..a735cc9d 100644 --- a/public/assets/tools/tcacademy.svg +++ b/public/assets/tools/tcacademy.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/assets/tools/topcrowd.svg b/public/assets/tools/topcrowd.svg deleted file mode 100644 index 79e0d9a3..00000000 --- a/public/assets/tools/topcrowd.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/assets/tools/wallet-admin.svg b/public/assets/tools/wallet-admin.svg new file mode 100644 index 00000000..68e5c04d --- /dev/null +++ b/public/assets/tools/wallet-admin.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/work-app.svg b/public/assets/tools/work-app.svg new file mode 100644 index 00000000..47f28069 --- /dev/null +++ b/public/assets/tools/work-app.svg @@ -0,0 +1 @@ + diff --git a/public/assets/tools/work-manager.svg b/public/assets/tools/work-manager.svg deleted file mode 100644 index ef1c5b25..00000000 --- a/public/assets/tools/work-manager.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/lib/components/tool-selector/ToolMenu.module.scss b/src/lib/components/tool-selector/ToolMenu.module.scss index e8f19bdc..83d11347 100644 --- a/src/lib/components/tool-selector/ToolMenu.module.scss +++ b/src/lib/components/tool-selector/ToolMenu.module.scss @@ -1,17 +1,19 @@ @use 'lib/styles/mixins.scss' as *; @use 'lib/styles/fonts.scss' as *; -@use 'lib/styles/colors.scss' as *; .toolMenuWrap { - width: 472px; + width: 496px; + // completes the popup's vertical padding to 32px + padding: 8px 0; - font-family: $roboto; + font-family: $nunito; font-style: normal; font-size: 14px; - color: #767676; + color: #6F6F6F; @include mobile { width: auto; + padding: 0; } > svg { @@ -20,19 +22,20 @@ } .title { - font-family: $roboto; + font-family: $nunito; font-style: normal; font-weight: 700; - font-size: 12px; - line-height: 16px; - letter-spacing: 1px; + font-size: 14px; + line-height: 20px; text-transform: uppercase; + color: #161616; } .toolSection { &Title { @extend .title; - margin-bottom: 16px; + line-height: 24px; + margin-bottom: 12px; } // hides the menu title when the section is alone in the whole menu @@ -55,19 +58,24 @@ } } -.toolGroups { +.toolColumns { display: flex; - gap: 24px; - flex-wrap: wrap; + gap: 48px; - .toolSection.talent & { + @include mobile { flex-direction: column; - max-height: 420px; + gap: 0; } +} + +.toolGroups { + display: flex; + flex-direction: column; + gap: 24px; + flex: 1 1 0; + min-width: 0; @include mobile { - flex-direction: column; - flex-wrap: nowrap; gap: 0; } } @@ -77,34 +85,26 @@ flex-direction: column; align-items: flex-start; - flex: 1 1 calc(50% - 24px); - &Title { @extend .title; - font-weight: 400; + font-weight: 600; + font-size: 12px; + line-height: 16px; + color: #6F6F6F; - margin-bottom: 12px; + margin-bottom: 8px; } @include mobile { padding: 16px; - order: var(--order); - - &.hasCtas { - border-top: 1px solid #D4D4D4; - } } } .toolNavItems { + display: flex; + flex-direction: column; + gap: 8px; width: 100%; - line-height: 1; - - @include mobile { - display: flex; - flex-direction: column; - align-items: flex-start; - } } .toolNavItem { @@ -112,57 +112,21 @@ align-items: center; gap: 8px; width: 100%; - overflow: hidden; - &.toolNavItem { - font-weight: 500; - color: color(hoverMenuLink); - } - - & + & { - margin-top: 12px; - } - - &:global(.navButton) { - display: inline-flex; - font-weight: 700; - font-size: 12px; - line-height: 24px; - letter-spacing: 0.008em; - text-transform: uppercase; - - color: #16679A; - border: 2px solid #16679A; - border-radius: 24px; - - padding: 0 16px; - width: auto; - - &:hover { - color: #219174; - border-color: #219174; - } - - .navItemLabel { - margin-top: 0; - margin-bottom: 0; - } - } - - &:hover { - font-weight: 700; - color: #0c0c0c; - color: color(hoverMenuLink, hover); + height: 32px; + // the description is revealed for the hovered (or keyboard focused) tool only + &:hover, + &:focus-visible { .navItemDescription { + max-height: 14px; opacity: 1; - pointer-events:unset; - font-weight: 400; - color: #767676; } } } .toolIcon { + flex: 0 0 auto; + img, svg { display: block; width: 32px; @@ -173,23 +137,32 @@ .navItemContent { display: flex; flex-direction: column; - margin: -3px 0 -4px; + min-width: 0; } .navItemLabel { display: block; - line-height: 22px; - margin-bottom: -4px; - margin-top: 4px; + font-size: 14px; + line-height: 18px; + color: #161616; + white-space: nowrap; } .navItemDescription { + display: block; font-style: italic; - font-size: 10px; - line-height: 18px; + font-size: 12px; + line-height: 14px; + color: #6F6F6F; + white-space: nowrap; + + max-height: 0; + // collapse vertically only, long descriptions may run into the columns gap + overflow-x: visible; + overflow-y: clip; opacity: 0; pointer-events: none; - transition: 0.2s ease-in-out; + transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out; } .toolMenuSpacer { diff --git a/src/lib/components/tool-selector/ToolMenu.svelte b/src/lib/components/tool-selector/ToolMenu.svelte index e19efad6..9632b95c 100644 --- a/src/lib/components/tool-selector/ToolMenu.svelte +++ b/src/lib/components/tool-selector/ToolMenu.svelte @@ -1,73 +1,68 @@
{#each navMenuItems as section, sectionIndex} {#if section} -
+
{section.label}
-
- {#each section.children ?? [] as group} -
- {#if group.label} -
- {group.label} -
- {/if} - -
- {#each group.children ?? [] as navItem} - - {#if navItem.type !== 'cta'} -
- {#if navItem.icon} - - - - {:else} - {navItem.label} - {/if} -
- {/if} -
diff --git a/src/lib/components/tool-selector/ToolSelector.svelte b/src/lib/components/tool-selector/ToolSelector.svelte index f71c55ad..50d5a5c4 100644 --- a/src/lib/components/tool-selector/ToolSelector.svelte +++ b/src/lib/components/tool-selector/ToolSelector.svelte @@ -1,6 +1,7 @@ -
popupIsVisible = true} - on:keydown={() => {}} -> - - - Tool - -
+{#if hasToolMenuItems} +
popupIsVisible = true} + on:keydown={() => {}} + > + + + Tool + +
-{#if !$isMobile} - - - -{:else if popupIsVisible} - popupIsVisible = false}> - - + {#if !$isMobile} + + + + {:else if popupIsVisible} + popupIsVisible = false}> + + + {/if} {/if} diff --git a/src/lib/config/hosts.ts b/src/lib/config/hosts.ts index 8ebdae49..072e7630 100644 --- a/src/lib/config/hosts.ts +++ b/src/lib/config/hosts.ts @@ -36,3 +36,21 @@ export const ACCOUNT_SETTINGS_HOST: string = `https://account-settings.${TC_DOMA export const WALLETAPP_HOST: string = `https://wallet.${TC_DOMAIN}`; export const COPILOT_PORTAL_HOST: string = `https://copilots.${TC_DOMAIN}`; export const ENGAGEMENT_PORTAL_HOST: string = `https://engagements.${TC_DOMAIN}`; + +// App drawer (tool selector) hosts +export const ARCHIVE_HOST: string = `https://archive.${TC_DOMAIN}`; +export const FORUMS_HOST: string = `https://forums.${TC_DOMAIN}`; +export const WORK_APP_HOST: string = `https://work.${TC_DOMAIN}`; +export const WALLET_ADMIN_HOST: string = `https://wallet-admin.${TC_DOMAIN}`; +export const CUSTOMER_PORTAL_HOST: string = `https://customer.${TC_DOMAIN}`; +export const SALES_HOST: string = `https://sales.${TC_DOMAIN}`; +export const PROCUREMENT_HOST: string = `https://procurement.${TC_DOMAIN}`; +export const ANALYTICS_HOST: string = `https://analytics.${TC_DOMAIN}`; +export const REPORTS_HOST: string = `https://reports.${TC_DOMAIN}`; +export const SUPPORT_HOST: string = `https://support.${TC_DOMAIN}`; +export const PAYLOAD_CMS_HOST: string = `https://cms.${TC_DOMAIN}`; +export const CONTACT_HOST: string = `https://contact.${TC_DOMAIN}`; +export const CAMPUS_HOST: string = `https://campus.${TC_DOMAIN}`; +export const CALENDAR_HOST: string = `https://calendar.${TC_DOMAIN}`; +export const SYSTEM_ADMIN_HOST: string = `https://system-admin.${TC_DOMAIN}`; +export const SALESFORCE_URL: string = 'https://topcoder.my.salesforce.com'; diff --git a/src/lib/config/nav-menu/all-nav-items.config.ts b/src/lib/config/nav-menu/all-nav-items.config.ts index aa550bd3..efab905e 100644 --- a/src/lib/config/nav-menu/all-nav-items.config.ts +++ b/src/lib/config/nav-menu/all-nav-items.config.ts @@ -4,16 +4,29 @@ import type { NavMenuItem } from "../../functions/nav-menu-item.model"; import { CHALLENGE_HOST, COMMUNITY_HOST, - CONNECT_HOST, COPILOT_PORTAL_HOST, REVIEW_APP_HOST, - TALENT_SEARCH_HOST, TCACADEMY_HOST, PLATFORM_APP_HOST, WALLETAPP_HOST, - WORK_MANAGER_HOST, AUTH0_AUTHENTICATOR_URL, ENGAGEMENT_PORTAL_HOST, + ARCHIVE_HOST, + FORUMS_HOST, + WORK_APP_HOST, + WALLET_ADMIN_HOST, + CUSTOMER_PORTAL_HOST, + SALES_HOST, + PROCUREMENT_HOST, + ANALYTICS_HOST, + REPORTS_HOST, + SUPPORT_HOST, + PAYLOAD_CMS_HOST, + CONTACT_HOST, + CAMPUS_HOST, + CALENDAR_HOST, + SYSTEM_ADMIN_HOST, + SALESFORCE_URL, } from '..'; export const allNavItems: {[key: string]: NavMenuItem} = { @@ -42,21 +55,21 @@ export const allNavItems: {[key: string]: NavMenuItem} = { mmTournament: { label: 'Marathon Match Tournament', icon: 'mm-tournament', - description: 'Join the Excitement', + description: 'Compete for the MM championship', marketingPathname: '/marathon-match-tournament', url: getMarketingUrl('/marathon-match-tournament'), }, aiHub: { label: 'AI Hub', icon: 'ai-hub', - description: 'Collaborate, Compete, Create', + description: 'Build and compete with AI', marketingPathname: '/ai-hub', url: getMarketingUrl('/ai-hub'), }, copilotPortal: { label: 'Copilot Portal', icon: 'copilot-portal', - description: 'Copilot Opportunities', + description: 'Copilot opportunities', url: COPILOT_PORTAL_HOST, }, howItWorks: { @@ -93,9 +106,15 @@ export const allNavItems: {[key: string]: NavMenuItem} = { articles: { label: 'Articles', icon: 'articles', - description: 'Get insights and tips about Topcoder', + description: 'Tutorials and how-tos', url: `${COMMUNITY_HOST}/thrive`, }, + archive: { + label: 'Archive', + icon: 'archive', + description: 'Past problems & TCO history', + url: ARCHIVE_HOST, + }, blog: { label: 'Blog', url: getMarketingUrl('/blog'), @@ -124,6 +143,12 @@ export const allNavItems: {[key: string]: NavMenuItem} = { icon: 'discord', description: 'Chat live with members', }, + publicForums: { + label: 'Public Forums', + url: FORUMS_HOST, + icon: 'forums', + description: 'Q&A and discussions', + }, home: { label: 'Home', marketingPathname: '/', @@ -141,30 +166,12 @@ export const allNavItems: {[key: string]: NavMenuItem} = { icon: 'payments', description: 'Get paid', }, - practice: { - label: 'Practice', - icon: 'practice', - url: `${CHALLENGE_HOST}/challenges?search=practice&tracks%5BDS%5D=true&tracks%5BDes%5D=true&tracks%5BDev%5D=true&tracks%5BQA%5D=true&types%5B%5D=CH&types%5B%5D=F2F&types%5B%5D=TSK`, - description: 'Learn to compete', - }, review: { - label: 'Review', + label: 'Review App', url: REVIEW_APP_HOST, icon: 'review', description: 'Review submissions', }, - communityHome: { - label: 'Community Home', - description: 'Recent challenges and news.', - icon: 'topcrowd', - url: `${COMMUNITY_HOST}/home`, - }, - talentSearchApp: { - label: 'Meet our Talent', - icon: 'talent-search', - description: 'Find skilled talent for your next project', - url: TALENT_SEARCH_HOST, - }, talkToAnExpert: { label: 'Talk to an Expert', description: 'Speak with a Topcoder expert to get started.', @@ -177,17 +184,89 @@ export const allNavItems: {[key: string]: NavMenuItem} = { icon: 'tcacademy', description: 'Learn new skills', }, - workManager: { - label: 'Work Manager', - icon: 'work-manager', - description: 'Work Manager App', - url: `${WORK_MANAGER_HOST}`, - }, - staffConnect: { - label: 'Connect', - icon: 'connect', - description: 'Connect App', - url: `${CONNECT_HOST}`, + workApp: { + label: 'Work App', + icon: 'work-app', + description: 'Launch and manage work', + url: WORK_APP_HOST, + }, + walletAdmin: { + label: 'Wallet Admin', + icon: 'wallet-admin', + description: 'Manage member payouts', + url: WALLET_ADMIN_HOST, + }, + customerPortal: { + label: 'Customer Portal', + icon: 'customer-portal', + description: 'Talent, stats & showcases', + url: CUSTOMER_PORTAL_HOST, + }, + salesforce: { + label: 'Salesforce', + icon: 'salesforce', + description: 'Manage accounts & leads', + url: SALESFORCE_URL, + }, + salesPipeline: { + label: 'Sales Pipeline', + icon: 'sales-pipeline', + description: 'Track deals by stage', + url: SALES_HOST, + }, + procurement: { + label: 'Procurement', + icon: 'procurement', + description: 'Vendors and purchasing', + url: PROCUREMENT_HOST, + }, + analytics: { + label: 'Analytics', + icon: 'analytics', + description: 'Platform and member data', + url: ANALYTICS_HOST, + }, + reports: { + label: 'Reports', + icon: 'reports', + description: 'Pull data on demand', + url: REPORTS_HOST, + }, + supportApp: { + label: 'Support', + icon: 'support', + description: 'Handle member tickets', + url: SUPPORT_HOST, + }, + payloadCms: { + label: 'Payload CMS', + icon: 'payload-cms', + description: 'Edit site content', + url: `${PAYLOAD_CMS_HOST}/admin`, + }, + massEmail: { + label: 'Mass Email', + icon: 'mass-email', + description: 'Email members at scale', + url: CONTACT_HOST, + }, + campus: { + label: 'Campus', + icon: 'campus', + description: 'University programs', + url: CAMPUS_HOST, + }, + leaveTracker: { + label: 'Leave Tracker', + icon: 'leave-tracker', + description: 'Plan and track time off', + url: CALENDAR_HOST, + }, + systemAdmin: { + label: 'System Admin', + icon: 'system-admin', + description: 'Platform-wide admin tools', + url: SYSTEM_ADMIN_HOST, }, bugHunt: { label: 'Bug Hunt', diff --git a/src/lib/config/nav-menu/tool-selector-nav-items.ts b/src/lib/config/nav-menu/tool-selector-nav-items.ts index 9aceab55..59be6ec4 100644 --- a/src/lib/config/nav-menu/tool-selector-nav-items.ts +++ b/src/lib/config/nav-menu/tool-selector-nav-items.ts @@ -1,86 +1,120 @@ import type { NavMenuItem } from "../../functions/nav-menu-item.model"; -import { CUSTOMER_ROUTE_GUARD, STAFF_ROUTE_GUARD, TALENT_ROUTE_GUARD } from "../auth" +import { STAFF_ROUTE_GUARD, TALENT_ROUTE_GUARD } from "../auth" import { allNavItems } from "./all-nav-items.config" +/** + * App drawer (tool selector) menu. + * + * Structure: section (role guarded) -> columns -> groups -> nav items. + * Each section renders its columns side by side on desktop and stacked on mobile, + * so the groups listed in a column are displayed top to bottom in that column. + */ export const toolSelectorNavItems: NavMenuItem = { children: [ { - label: "Customer", + label: "Talent", children: [ { children: [ - allNavItems.talentSearchApp, + { + label: "Earn", + children: [ + allNavItems.challengesApp, + allNavItems.engagementsApp, + allNavItems.copilotPortal, + allNavItems.review, + allNavItems.payments, + ] + }, + { + label: "Compete", + children: [ + allNavItems.marathonMatchesApp, + allNavItems.mmTournament, + ] + }, ] }, { children: [ - allNavItems.talkToAnExpert, - allNavItems.bookADemo, + { + label: "AI", + children: [ + allNavItems.aiHub, + ] + }, + { + label: "Learn", + children: [ + allNavItems.topcoderAcademyApp, + { + ...allNavItems.articles, + url: `${allNavItems.articles.url}?navTool=tool` + }, + allNavItems.archive, + ] + }, + { + label: "Connect", + children: [ + allNavItems.publicForums, + allNavItems.discordApp, + ] + }, ] - } + }, ], - ...CUSTOMER_ROUTE_GUARD, + ...TALENT_ROUTE_GUARD, }, { - label: "Talent", + label: "Staff", children: [ { - label: "Learn", - groupOrder: 1, - children: [ - allNavItems.communityHome, - allNavItems.topcoderAcademyApp, - allNavItems.practice, - ] - }, - { - label: "Earn", - groupOrder: 2, - children: [ - allNavItems.challengesApp, - allNavItems.engagementsApp, - allNavItems.review, - allNavItems.payments, - allNavItems.copilotPortal, - ] - }, - { - label: "AI", - groupOrder: 3, - children: [ - allNavItems.aiHub, - ] - }, - { - label: "Compete", - groupOrder: 3, children: [ - allNavItems.marathonMatchesApp, - allNavItems.mmTournament, + { + label: "Delivery & Payments", + children: [ + allNavItems.workApp, + allNavItems.walletAdmin, + ] + }, + { + label: "Sales & Customers", + children: [ + allNavItems.customerPortal, + allNavItems.salesforce, + allNavItems.salesPipeline, + allNavItems.procurement, + ] + }, + { + label: "Insights", + children: [ + allNavItems.analytics, + allNavItems.reports, + ] + }, ] }, { - label: "Connect", - groupOrder: 4, children: [ { - ...allNavItems.articles, - url: `${allNavItems.articles.url}?navTool=tool` + label: "Community & Content", + children: [ + allNavItems.supportApp, + allNavItems.payloadCms, + allNavItems.massEmail, + allNavItems.campus, + ] + }, + { + label: "Team & Platform", + children: [ + allNavItems.leaveTracker, + allNavItems.systemAdmin, + ] }, - allNavItems.discordApp, - ] - } - ], - ...TALENT_ROUTE_GUARD, - }, - { - label: "Staff", - children: [ - { - children: [ - allNavItems.workManager, - allNavItems.staffConnect, ] }, ], diff --git a/tests/tool-selector-nav-items.test.mjs b/tests/tool-selector-nav-items.test.mjs new file mode 100644 index 00000000..4b1bc7c6 --- /dev/null +++ b/tests/tool-selector-nav-items.test.mjs @@ -0,0 +1,136 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { after, before, test } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { createServer } from 'vite'; + +const root = fileURLToPath(new URL('..', import.meta.url)); + +// expected app drawer (PM-6555 design): section -> columns -> groups -> tool labels +const TALENT = ['Talent', [ + [ + ['Earn', ['Opportunities', 'Engagement Portal', 'Copilot Portal', 'Review App', 'Wallet']], + ['Compete', ['Marathon Matches', 'Marathon Match Tournament']], + ], + [ + ['AI', ['AI Hub']], + ['Learn', ['Topcoder Academy', 'Articles', 'Archive']], + ['Connect', ['Public Forums', 'Discord']], + ], +]]; +const STAFF = ['Staff', [ + [ + ['Delivery & Payments', ['Work App', 'Wallet Admin']], + ['Sales & Customers', ['Customer Portal', 'Salesforce', 'Sales Pipeline', 'Procurement']], + ['Insights', ['Analytics', 'Reports']], + ], + [ + ['Community & Content', ['Support', 'Payload CMS', 'Mass Email', 'Campus']], + ['Team & Platform', ['Leave Tracker', 'System Admin']], + ], +]]; + +let server; +let getToolSelectorItems; + +before(async () => { + process.env.VITE_APP_HOST_ENV = 'prod'; + server = await createServer({ + root, + configFile: false, + logLevel: 'silent', + appType: 'custom', + resolve: { alias: { lib: `${root}src/lib` } }, + server: { middlewareMode: true, hmr: false, watch: null }, + }); + ({ getToolSelectorItems } = await server.ssrLoadModule('/src/lib/functions/tool-selector-menu.provider.ts')); +}); + +after(() => server?.close()); + +/** + * Stubs the browser globals read by the jwt helpers with a `tcjwt` cookie holding the given roles. + * @param {string[]} roles JWT roles of the signed in user + */ +function signIn(roles) { + const payload = Buffer.from(JSON.stringify({ 'https://topcoder.com/roles': roles })).toString('base64url'); + globalThis.window = { atob: globalThis.atob }; + globalThis.document = { cookie: `tcjwt=e30.${payload}.signature` }; +} + +/** Reduces the drawer menu to [section, [column[[group, labels]]]] for comparison with the design. */ +const toLayout = (sections) => sections.map((section) => [ + section.label, + section.children.map((column) => column.children.map((group) => [ + group.label, + group.children.map((item) => item.label), + ])), +]); + +const toItems = (sections) => sections.flatMap((section) => section.children + .flatMap((column) => column.children.flatMap((group) => group.children))); + +test('staff see the Talent and Staff sections laid out as in the design', () => { + signIn(['Topcoder User', 'Topcoder Staff']); + assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT, STAFF]); +}); + +test('members only see the Talent section', () => { + for (const roles of [['Topcoder User'], ['Topcoder User', 'Topcoder Talent']]) { + signIn(roles); + assert.deepEqual(toLayout(getToolSelectorItems()), [TALENT]); + } +}); + +test('customers without talent or staff roles get no drawer items', () => { + signIn(['Topcoder Customer']); + assert.deepEqual(getToolSelectorItems(), []); +}); + +test('every drawer tool has a description, a sprite icon and its production url', async () => { + const sprite = await readFile(`${root}public/assets/tools/sprite.svg`, 'utf8'); + signIn(['Topcoder Staff']); + const items = toItems(getToolSelectorItems()); + + for (const item of items) { + assert.ok(item.description, `${item.label} has a description`); + assert.ok(sprite.includes(`id="${item.icon}"`), `${item.label} icon "${item.icon}" is in the sprite`); + } + + const urls = Object.fromEntries(items.map((item) => [item.label, item.url])); + assert.deepEqual({ + 'Archive': urls['Archive'], + 'Public Forums': urls['Public Forums'], + 'Work App': urls['Work App'], + 'Wallet Admin': urls['Wallet Admin'], + 'Customer Portal': urls['Customer Portal'], + 'Salesforce': urls['Salesforce'], + 'Sales Pipeline': urls['Sales Pipeline'], + 'Procurement': urls['Procurement'], + 'Analytics': urls['Analytics'], + 'Reports': urls['Reports'], + 'Support': urls['Support'], + 'Payload CMS': urls['Payload CMS'], + 'Mass Email': urls['Mass Email'], + 'Campus': urls['Campus'], + 'Leave Tracker': urls['Leave Tracker'], + 'System Admin': urls['System Admin'], + }, { + 'Archive': 'https://archive.topcoder.com', + 'Public Forums': 'https://forums.topcoder.com', + 'Work App': 'https://work.topcoder.com', + 'Wallet Admin': 'https://wallet-admin.topcoder.com', + 'Customer Portal': 'https://customer.topcoder.com', + 'Salesforce': 'https://topcoder.my.salesforce.com', + 'Sales Pipeline': 'https://sales.topcoder.com', + 'Procurement': 'https://procurement.topcoder.com', + 'Analytics': 'https://analytics.topcoder.com', + 'Reports': 'https://reports.topcoder.com', + 'Support': 'https://support.topcoder.com', + 'Payload CMS': 'https://cms.topcoder.com/admin', + 'Mass Email': 'https://contact.topcoder.com', + 'Campus': 'https://campus.topcoder.com', + 'Leave Tracker': 'https://calendar.topcoder.com', + 'System Admin': 'https://system-admin.topcoder.com', + }); +}); diff --git a/types/src/lib/config/hosts.d.ts b/types/src/lib/config/hosts.d.ts index 0c5526f8..503bf21e 100644 --- a/types/src/lib/config/hosts.d.ts +++ b/types/src/lib/config/hosts.d.ts @@ -19,3 +19,19 @@ export declare const ACCOUNT_SETTINGS_HOST: string; export declare const WALLETAPP_HOST: string; export declare const COPILOT_PORTAL_HOST: string; export declare const ENGAGEMENT_PORTAL_HOST: string; +export declare const ARCHIVE_HOST: string; +export declare const FORUMS_HOST: string; +export declare const WORK_APP_HOST: string; +export declare const WALLET_ADMIN_HOST: string; +export declare const CUSTOMER_PORTAL_HOST: string; +export declare const SALES_HOST: string; +export declare const PROCUREMENT_HOST: string; +export declare const ANALYTICS_HOST: string; +export declare const REPORTS_HOST: string; +export declare const SUPPORT_HOST: string; +export declare const PAYLOAD_CMS_HOST: string; +export declare const CONTACT_HOST: string; +export declare const CAMPUS_HOST: string; +export declare const CALENDAR_HOST: string; +export declare const SYSTEM_ADMIN_HOST: string; +export declare const SALESFORCE_URL: string; diff --git a/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts b/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts index 6b23ef7e..db1ca51e 100644 --- a/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts +++ b/types/src/lib/config/nav-menu/tool-selector-nav-items.d.ts @@ -1,2 +1,9 @@ import type { NavMenuItem } from "../../functions/nav-menu-item.model"; +/** + * App drawer (tool selector) menu. + * + * Structure: section (role guarded) -> columns -> groups -> nav items. + * Each section renders its columns side by side on desktop and stacked on mobile, + * so the groups listed in a column are displayed top to bottom in that column. + */ export declare const toolSelectorNavItems: NavMenuItem;