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;