From 2a3cf060965205788a80670662afb6717b96df30 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 24 Aug 2026 09:27:13 +0530 Subject: [PATCH 1/2] Integrate UI with skill statistics api --- .../lib/services/statistics.service.spec.ts | 73 ++++ .../src/lib/services/statistics.service.ts | 55 +++ .../SkillStatisticsPage/SkillBubblesChart.tsx | 41 +- .../SkillStatisticsPage/SkillMembersPanel.tsx | 6 +- .../SkillStatisticsPage.module.scss | 14 + .../SkillStatisticsPage.spec.tsx | 156 +++++++- .../SkillStatisticsPage.tsx | 98 ++++- .../SkillStatisticsPage/mock/index.ts | 2 - .../mock/skill-categories.mock.ts | 377 ------------------ .../mock/skill-members.mock.ts | 192 --------- 10 files changed, 390 insertions(+), 624 deletions(-) create mode 100644 src/apps/customer-portal/src/lib/services/statistics.service.spec.ts delete mode 100644 src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/mock/index.ts delete mode 100644 src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/mock/skill-categories.mock.ts delete mode 100644 src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/mock/skill-members.mock.ts diff --git a/src/apps/customer-portal/src/lib/services/statistics.service.spec.ts b/src/apps/customer-portal/src/lib/services/statistics.service.spec.ts new file mode 100644 index 000000000..d48d22b27 --- /dev/null +++ b/src/apps/customer-portal/src/lib/services/statistics.service.spec.ts @@ -0,0 +1,73 @@ +import { xhrGetAsync } from '~/libs/core' + +import { + fetchExpertSkillCategories, + fetchExpertSkillCategoryMembers, +} from './statistics.service' + +jest.mock('~/config', () => ({ + EnvironmentConfig: { + API: { V6: 'https://api.example.com/v6' }, + REPORTS_API: 'https://reports.example.com', + }, +}), { + virtual: true, +}) + +jest.mock('~/libs/core', () => ({ + xhrGetAsync: jest.fn(), +}), { + virtual: true, +}) + +const mockedXhrGetAsync = xhrGetAsync as jest.MockedFunction + +describe('statistics.service expert-skills', () => { + beforeEach(() => { + mockedXhrGetAsync.mockReset() + }) + + it('loads skill categories from statistics/expert-skills', async () => { + const categories = [{ + color: '#1B4F72', + icon: 'TerminalIcon', + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + officialName: 'Programming and Development', + size: 10, + skillsBreakdown: [{ name: 'JavaScript', percentage: 40 }], + totalMembers: 101, + totalSkills: 50, + }] + mockedXhrGetAsync.mockResolvedValueOnce(categories) + + await expect(fetchExpertSkillCategories()) + .resolves + .toEqual(categories) + expect(mockedXhrGetAsync) + .toHaveBeenCalledWith( + 'https://reports.example.com/statistics/expert-skills/categories', + ) + }) + + it('loads category members from statistics/expert-skills', async () => { + const members = [{ + countryCode: 'IN', + countryName: 'India', + handle: 'billzedison', + name: 'Honghan W', + rating: 2000, + wins: 376, + }] + mockedXhrGetAsync.mockResolvedValueOnce(members) + + await expect(fetchExpertSkillCategoryMembers('Programming and Development')) + .resolves + .toEqual(members) + expect(mockedXhrGetAsync) + .toHaveBeenCalledWith( + 'https://reports.example.com/statistics/expert-skills/category-members' + + '?selectedcategory=Programming+and+Development', + ) + }) +}) diff --git a/src/apps/customer-portal/src/lib/services/statistics.service.ts b/src/apps/customer-portal/src/lib/services/statistics.service.ts index e15a59c15..c00df244e 100644 --- a/src/apps/customer-portal/src/lib/services/statistics.service.ts +++ b/src/apps/customer-portal/src/lib/services/statistics.service.ts @@ -92,6 +92,7 @@ type CountryLookupResponse = { } const GENERAL_STATISTICS_URL = `${EnvironmentConfig.REPORTS_API}/statistics/general` +const EXPERT_SKILLS_STATISTICS_URL = `${EnvironmentConfig.REPORTS_API}/statistics/expert-skills` const COUNTRY_LOOKUP_URL = `${EnvironmentConfig.API.V6}/lookups/countries?page=1&perPage=9999` const COUNTRY_NAME_ALIASES: Record = { @@ -263,3 +264,57 @@ export async function fetchGeneralStatistics(): Promise { totalPrizes: Number(totalPrizesResponse.total || 0), } } + +export type ExpertSkillBreakdown = { + name: string + percentage: number +} + +export type ExpertSkillCategory = { + color: string + icon: string + id: string + name: string + officialName: string + size: number + skillsBreakdown: ExpertSkillBreakdown[] + totalMembers: number + totalSkills: number +} + +export type ExpertSkillCategoryMember = { + countryCode: string + countryName: string + handle: string + name: string + photoURL?: string | null + rating: number + wins: number +} + +export const EXPERT_SKILL_CATEGORIES_CACHE_KEY = 'customer-portal-expert-skill-categories' + +export function expertSkillCategoryMembersCacheKey(selectedCategory: string): string { + return `customer-portal-expert-skill-category-members:${selectedCategory}` +} + +export async function fetchExpertSkillCategories(): Promise { + const response = await xhrGetAsync( + `${EXPERT_SKILLS_STATISTICS_URL}/categories`, + ) + + return Array.isArray(response) ? response : [] +} + +export async function fetchExpertSkillCategoryMembers( + selectedCategory: string, +): Promise { + const query = new URLSearchParams({ + selectedcategory: selectedCategory, + }) + const response = await xhrGetAsync( + `${EXPERT_SKILLS_STATISTICS_URL}/category-members?${query.toString()}`, + ) + + return Array.isArray(response) ? response : [] +} diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx index a4e6b022f..09f395788 100644 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx +++ b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx @@ -4,6 +4,7 @@ import { FC, KeyboardEvent, RefObject, + SVGProps, useCallback, useEffect, useLayoutEffect, @@ -13,17 +14,20 @@ import { } from 'react' import { createPortal } from 'react-dom' import classNames from 'classnames' +import useSWR, { SWRResponse } from 'swr' import { getRatingColor } from '~/libs/core' +import { IconOutline } from '~/libs/ui' +import { + ExpertSkillCategory, + ExpertSkillCategoryMember, + expertSkillCategoryMembersCacheKey, + fetchExpertSkillCategoryMembers, +} from '../../../lib' import memberGroupIcon from '../../statistics/StatisticsPage/assets/member-group.svg' import skillCognitionIcon from '../../statistics/StatisticsPage/assets/skill-cognition.svg' -import { - getTopMemberForCategory, - SkillCategoryMock, - SkillMemberMock, -} from './mock' import { packCircles, PackedCircle } from './packCircles' import styles from './SkillBubblesChart.module.scss' @@ -118,12 +122,21 @@ function getPopoverLayout( } } +type SkillCategoryIcon = FC> + interface SkillBubblesChartProps { - categories: SkillCategoryMock[] + categories: ExpertSkillCategory[] onSelect: (categoryId: string) => void selectedCategoryId?: string } +function getCategoryIcon(iconName?: string): SkillCategoryIcon { + const icons = IconOutline as Record + const icon = iconName ? icons[iconName] : undefined + + return icon || IconOutline.CodeIcon +} + function radiusForSize(size: number): number { return 28 + (size * 9) } @@ -207,9 +220,13 @@ const SkillBubblesChart: FC = props => { const hoveredCircle = hoveredCategory ? packedById.get(hoveredCategory.id) : undefined - const topMember = hoveredCategory - ? getTopMemberForCategory(hoveredCategory.id) - : undefined + const { data: hoveredMembers }: SWRResponse = useSWR( + hoveredCategory + ? expertSkillCategoryMembersCacheKey(hoveredCategory.name) + : undefined, + () => fetchExpertSkillCategoryMembers(hoveredCategory?.name || ''), + ) + const topMember = hoveredMembers?.[0] const handleKeyDown = useCallback(( event: KeyboardEvent, @@ -234,7 +251,7 @@ const SkillBubblesChart: FC = props => { return undefined } - const Icon = category.icon + const Icon = getCategoryIcon(category.icon) const isSelected = category.id === props.selectedCategoryId const fontSize = fontSizeForRadius( circle.r, @@ -291,12 +308,12 @@ const SkillBubblesChart: FC = props => { } interface SkillCategoryPopoverProps { - category: SkillCategoryMock + category: ExpertSkillCategory chartHeight: number chartRef: RefObject chartWidth: number circle: PackedCircle - topMember?: SkillMemberMock + topMember?: ExpertSkillCategoryMember } const SkillCategoryPopover = (props: SkillCategoryPopoverProps): JSX.Element => { diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillMembersPanel.tsx b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillMembersPanel.tsx index a927bab44..8776462d1 100644 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillMembersPanel.tsx +++ b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillMembersPanel.tsx @@ -7,21 +7,21 @@ import { getRatingColor } from '~/libs/core' import { ProfilePicture } from '~/libs/shared' import { IconOutline } from '~/libs/ui' +import { ExpertSkillCategory, ExpertSkillCategoryMember } from '../../../lib' import { IconFirstPlace, IconSecondPlace, IconThirdPlace, } from '../../statistics/StatisticsPage/assets' -import { SkillCategoryMock, SkillMemberMock } from './mock' import styles from './SkillMembersPanel.module.scss' const NUMBER_FORMATTER = new Intl.NumberFormat('en-US') interface SkillMembersPanelProps { - category: SkillCategoryMock + category: ExpertSkillCategory countryFilter: string - members: SkillMemberMock[] + members: ExpertSkillCategoryMember[] onCountryChange: (countryCode: string) => void onSearchChange: (value: string) => void search: string diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.module.scss b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.module.scss index f55ffd6be..b7a1e854d 100644 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.module.scss +++ b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.module.scss @@ -48,3 +48,17 @@ margin: 20px 0 8px; text-align: center; } + +.status { + align-items: center; + color: #545f71; + display: flex; + gap: 8px; + justify-content: center; + min-height: 240px; + + button { + color: #0d61bf; + text-decoration: underline; + } +} diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.spec.tsx b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.spec.tsx index afedc26e3..3636316a1 100644 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.spec.tsx +++ b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.spec.tsx @@ -1,9 +1,13 @@ /* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */ import '@testing-library/jest-dom' import { fireEvent, render, screen, within } from '@testing-library/react' +import { SWRConfig } from 'swr' import { getTabIdFromPathName, getTabsConfig } from '../../../lib/components/NavTabs/config/tabs-config' -import { SKILL_CATEGORIES } from './mock' +import { + fetchExpertSkillCategories, + fetchExpertSkillCategoryMembers, +} from '../../../lib' import SkillStatisticsPage from './SkillStatisticsPage' jest.mock('~/config', () => ({ @@ -71,6 +75,87 @@ jest.mock('../../statistics/StatisticsPage/assets/skill-cognition.svg', () => 's virtual: true, }) +jest.mock('../../../lib', () => ({ + EXPERT_SKILL_CATEGORIES_CACHE_KEY: 'customer-portal-expert-skill-categories', + expertSkillCategoryMembersCacheKey: (selectedCategory: string) => ( + `customer-portal-expert-skill-category-members:${selectedCategory}` + ), + fetchExpertSkillCategories: jest.fn(), + fetchExpertSkillCategoryMembers: jest.fn(), +})) + +const mockedFetchCategories = fetchExpertSkillCategories as jest.MockedFunction< + typeof fetchExpertSkillCategories +> +const mockedFetchMembers = fetchExpertSkillCategoryMembers as jest.MockedFunction< + typeof fetchExpertSkillCategoryMembers +> + +const CATEGORIES = [ + { + color: '#1B4F72', + icon: 'TerminalIcon', + id: '481b5ebc-2fe6-45ed-a90c-736936d458d7', + name: 'Programming and Development', + officialName: 'Programming and Development', + size: 10, + skillsBreakdown: [{ name: 'JavaScript', percentage: 40 }], + totalMembers: 101, + totalSkills: 50, + }, + { + color: '#4A6A7A', + icon: 'CodeIcon', + id: '1f5ed3e8-8d22-44ea-b75d-ea85147a04da', + name: 'Scripting and Automation', + officialName: 'Scripting and Automation', + size: 3, + skillsBreakdown: [], + totalMembers: 10, + totalSkills: 4, + }, +] + +const MEMBERS = [ + { + countryCode: 'IN', + countryName: 'India', + handle: 'billzedison', + name: 'Honghan W', + rating: 2000, + wins: 376, + }, + { + countryCode: 'US', + countryName: 'USA', + handle: 'Ghostar', + name: 'Justin G', + rating: 1900, + wins: 322, + }, + { + countryCode: 'GB', + countryName: 'UK', + handle: 'diazx', + name: 'DAT N', + rating: 2300, + wins: 200, + }, +] + +function renderPage(): ReturnType { + return render( + new Map(), + }} + > + + , + ) +} + describe('Customer Portal Skill Statistics tabs', () => { it('adds Skill Statistics beside General Statistics', () => { const tabs = getTabsConfig(['administrator'], false, false) @@ -91,46 +176,60 @@ describe('Customer Portal Skill Statistics tabs', () => { }) describe('SkillStatisticsPage', () => { - it('renders all 23 skill categories', () => { - render() + beforeEach(() => { + mockedFetchCategories.mockReset() + mockedFetchMembers.mockReset() + mockedFetchCategories.mockResolvedValue(CATEGORIES) + mockedFetchMembers.mockImplementation(async selectedCategory => ( + selectedCategory === 'Programming and Development' ? MEMBERS : [] + )) + }) + + it('renders skill categories from the reports API', async () => { + renderPage() - expect(SKILL_CATEGORIES) - .toHaveLength(23) + expect(await screen.findByRole('button', { name: 'Programming and Development' })) + .toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Scripting and Automation' })) + .toBeInTheDocument() + expect(screen.getByText('Browse and connect with verified experts across 2 skill categories.')) + .toBeInTheDocument() expect(screen.queryByRole('button', { name: 'Bar' })) .not.toBeInTheDocument() - SKILL_CATEGORIES.forEach(category => { - expect(screen.getByRole('button', { name: category.name })) - .toBeInTheDocument() - }) + expect(mockedFetchCategories) + .toHaveBeenCalledTimes(1) }) - it('shows the category popover on hover and the members UI on click', () => { - render() + it('shows the category popover on hover and the members UI on click', async () => { + renderPage() - const bubble = screen.getByRole('button', { name: 'Programming & Development' }) + const bubble = await screen.findByRole('button', { name: 'Programming and Development' }) fireEvent.mouseEnter(bubble) expect(screen.getByText('Total Members')) .toBeInTheDocument() - expect(screen.getByText('banerjeesourish')) + expect(await screen.findByText('billzedison')) .toBeInTheDocument() expect(screen.getByText('Total Members') .closest('[data-placement]')) .toHaveAttribute('data-placement', expect.stringMatching(/^(top|bottom|left|right)$/)) - expect(screen.queryByRole('heading', { name: 'Members for Programming & Development' })) + expect(screen.queryByRole('heading', { name: 'Members for Programming and Development' })) .not.toBeInTheDocument() fireEvent.click(bubble) - expect(screen.getByRole('heading', { name: 'Members for Programming & Development' })) + expect(await screen.findByRole('heading', { name: 'Members for Programming and Development' })) .toBeInTheDocument() - expect(screen.getByText('billzedison')) + expect(screen.getByText('Ghostar')) .toBeInTheDocument() }) - it('filters members from in-memory state when searching', () => { - render() - fireEvent.click(screen.getByRole('button', { name: 'Programming & Development' })) + it('filters members from in-memory state when searching', async () => { + renderPage() + fireEvent.click(await screen.findByRole('button', { name: 'Programming and Development' })) + + expect(await screen.findByText('billzedison')) + .toBeInTheDocument() fireEvent.change(screen.getByLabelText('Search members'), { target: { value: 'Ghostar' }, @@ -148,9 +247,12 @@ describe('SkillStatisticsPage', () => { .toBeInTheDocument() }) - it('reranks members when filtering by country', () => { - render() - fireEvent.click(screen.getByRole('button', { name: 'Programming & Development' })) + it('reranks members when filtering by country', async () => { + renderPage() + fireEvent.click(await screen.findByRole('button', { name: 'Programming and Development' })) + + expect(await screen.findByText('billzedison')) + .toBeInTheDocument() fireEvent.change(screen.getByLabelText('Filter By'), { target: { value: 'GB' }, @@ -167,4 +269,14 @@ describe('SkillStatisticsPage', () => { .getByText('1st')) .toBeInTheDocument() }) + + it('shows an error when skill categories fail to load', async () => { + mockedFetchCategories.mockRejectedValueOnce(new Error('failed')) + renderPage() + + expect(await screen.findByRole('alert')) + .toHaveTextContent('Skill categories could not be loaded.') + expect(screen.queryByRole('button', { name: 'Programming and Development' })) + .not.toBeInTheDocument() + }) }) diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.tsx b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.tsx index a85e28e49..53fb628b2 100644 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.tsx +++ b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillStatisticsPage.tsx @@ -1,54 +1,120 @@ import { FC, useCallback, useMemo, useState } from 'react' +import useSWR, { SWRResponse } from 'swr' import 'flag-icons/css/flag-icons.min.css' -import { getMembersForCategory, SKILL_CATEGORIES } from './mock' +import { + ExpertSkillCategory, + ExpertSkillCategoryMember, + expertSkillCategoryMembersCacheKey, + EXPERT_SKILL_CATEGORIES_CACHE_KEY, + fetchExpertSkillCategories, + fetchExpertSkillCategoryMembers, +} from '../../../lib' + import SkillBubblesChart from './SkillBubblesChart' import SkillMembersPanel from './SkillMembersPanel' import styles from './SkillStatisticsPage.module.scss' +const NUMBER_FORMATTER = new Intl.NumberFormat('en-US') + +function getPageSubtitle(categoryCount?: number): string { + if (!categoryCount) { + return 'Browse and connect with verified experts.' + } + + return `Browse and connect with verified experts across ${NUMBER_FORMATTER.format(categoryCount)} skill categories.` +} + const SkillStatisticsPage: FC = () => { const [selectedCategoryId, setSelectedCategoryId] = useState() const [search, setSearch] = useState('') const [countryFilter, setCountryFilter] = useState('') + const { + data: categories, + error: categoriesError, + mutate: reloadCategories, + }: SWRResponse = useSWR( + EXPERT_SKILL_CATEGORIES_CACHE_KEY, + fetchExpertSkillCategories, + ) const selectedCategory = useMemo( - () => SKILL_CATEGORIES.find(category => category.id === selectedCategoryId), - [selectedCategoryId], + () => categories?.find(category => category.id === selectedCategoryId), + [categories, selectedCategoryId], ) - const selectedMembers = useMemo( - () => (selectedCategoryId ? getMembersForCategory(selectedCategoryId) : []), - [selectedCategoryId], + const { + data: members, + error: membersError, + mutate: reloadMembers, + }: SWRResponse = useSWR( + selectedCategory + ? expertSkillCategoryMembersCacheKey(selectedCategory.name) + : undefined, + () => fetchExpertSkillCategoryMembers(selectedCategory?.name || ''), ) + const isLoadingCategories = !categories && !categoriesError + const isLoadingMembers = Boolean(selectedCategory && !members && !membersError) + const selectCategory = useCallback((categoryId: string) => { setSelectedCategoryId(categoryId) setSearch('') setCountryFilter('') }, []) + const retryCategories = useCallback(() => { + reloadCategories() + }, [reloadCategories]) + + const retryMembers = useCallback(() => { + reloadMembers() + }, [reloadMembers]) + return (

Skill Statistics

-

- Browse and connect with verified experts across 23 skill categories. -

+

{getPageSubtitle(categories?.length)}

Select a skill category to see additional details

- + {isLoadingCategories && ( +
Loading skill categories…
+ )} + {categoriesError && ( +
+ Skill categories could not be loaded. + +
+ )} + {!isLoadingCategories && !categoriesError && ( + + )}
- {selectedCategory && ( + {selectedCategory && isLoadingMembers && ( +
Loading members…
+ )} + {selectedCategory && membersError && ( +
+ Members could not be loaded. + +
+ )} + {selectedCategory && !isLoadingMembers && !membersError && ( > - -export type SkillCategoryMock = { - color: string - icon: SkillCategoryIcon - id: string - name: string - officialName: string - size: number - skillsBreakdown: Array<{ name: string; percentage: number }> - totalMembers: number - totalSkills: number -} - -export type SkillMemberMock = { - countryCode: string - countryName: string - handle: string - name: string - photoURL?: string - rating: number - wins: number -} - -export const PROGRAMMING_CATEGORY_ID = '481b5ebc-2fe6-45ed-a90c-736936d458d7' - -export const SKILL_CATEGORIES: SkillCategoryMock[] = [ - { - color: '#1B4F72', - icon: IconOutline.TerminalIcon, - id: PROGRAMMING_CATEGORY_ID, - name: 'Programming & Development', - officialName: 'Programming and Development', - size: 10, - skillsBreakdown: [ - { name: 'JavaScript', percentage: 40 }, - { name: 'Python', percentage: 30 }, - { name: 'Swift', percentage: 15 }, - ], - totalMembers: 1012928, - totalSkills: 1059, - }, - { - color: '#3D8B8F', - icon: IconOutline.RssIcon, - id: 'cfb17211-2abd-41e1-b169-e90cf038c6a7', - name: 'Networking and Telecommunications', - officialName: 'Networking and Telecommunications', - size: 8.4, - skillsBreakdown: [ - { name: 'TCP/IP', percentage: 35 }, - { name: 'Routing', percentage: 28 }, - { name: '5G', percentage: 22 }, - ], - totalMembers: 412800, - totalSkills: 286, - }, - { - color: '#5B9BD5', - icon: IconOutline.GlobeAltIcon, - id: '5aadafad-da63-488e-8499-32b596215789', - name: 'Web Development', - officialName: 'Web Development', - size: 7.8, - skillsBreakdown: [ - { name: 'React', percentage: 38 }, - { name: 'Node.js', percentage: 27 }, - { name: 'CSS', percentage: 20 }, - ], - totalMembers: 388420, - totalSkills: 412, - }, - { - color: '#5EB3C4', - icon: IconOutline.ShieldCheckIcon, - id: '221f4e3f-1ac8-438b-9dc1-977e30656789', - name: 'Cybersecurity', - officialName: 'Cybersecurity', - size: 7.2, - skillsBreakdown: [ - { name: 'Pen Testing', percentage: 32 }, - { name: 'SIEM', percentage: 26 }, - { name: 'IAM', percentage: 24 }, - ], - totalMembers: 276540, - totalSkills: 198, - }, - { - color: '#1A3D3D', - icon: IconOutline.CloudIcon, - id: 'cc346829-c9e4-44a9-996b-34054cf20fec', - name: 'Cloud Computing', - officialName: 'Cloud Computing', - size: 6.4, - skillsBreakdown: [ - { name: 'AWS', percentage: 42 }, - { name: 'Azure', percentage: 28 }, - { name: 'GCP', percentage: 18 }, - ], - totalMembers: 241100, - totalSkills: 176, - }, - { - color: '#2D4A3E', - icon: IconOutline.RefreshIcon, - id: 'aa495f25-2f2d-4334-9b6f-2ffe11d835d2', - name: 'Software Development Lifecycle', - officialName: 'Software Development Lifecycle (SDLC)', - size: 6.2, - skillsBreakdown: [ - { name: 'Agile', percentage: 40 }, - { name: 'CI/CD', percentage: 30 }, - { name: 'Scrum', percentage: 18 }, - ], - totalMembers: 198760, - totalSkills: 94, - }, - { - color: '#4A5D4A', - icon: IconOutline.DuplicateIcon, - id: 'e2429c1b-7609-49e0-93cc-341a89e12269', - name: 'DevOps & Automation', - officialName: 'DevOps and Automation', - size: 5.8, - skillsBreakdown: [ - { name: 'Kubernetes', percentage: 34 }, - { name: 'Terraform', percentage: 28 }, - { name: 'Jenkins', percentage: 22 }, - ], - totalMembers: 176430, - totalSkills: 142, - }, - { - color: '#3D7EA6', - icon: IconOutline.ChipIcon, - id: '185f4bf3-50de-46af-aaa6-9011872395cf', - name: 'Operating Systems', - officialName: 'Operating Systems', - size: 5.5, - skillsBreakdown: [ - { name: 'Linux', percentage: 48 }, - { name: 'Windows', percentage: 22 }, - { name: 'macOS', percentage: 16 }, - ], - totalMembers: 154220, - totalSkills: 88, - }, - { - color: '#2C5F8A', - icon: IconOutline.ChartBarIcon, - id: '4064574c-befa-4fb3-a8e2-34038d7f845b', - name: 'Data Analysis & Big Data', - officialName: 'Data Analysis and Big Data', - size: 5.4, - skillsBreakdown: [ - { name: 'SQL', percentage: 36 }, - { name: 'Spark', percentage: 26 }, - { name: 'Tableau', percentage: 20 }, - ], - totalMembers: 148900, - totalSkills: 164, - }, - { - color: '#7EB8C4', - icon: IconOutline.SparklesIcon, - id: 'a1289278-a734-4523-918f-ea0f05667e24', - name: 'Machine Learning & AI', - officialName: 'Machine Learning and AI', - size: 5.1, - skillsBreakdown: [ - { name: 'PyTorch', percentage: 33 }, - { name: 'TensorFlow', percentage: 29 }, - { name: 'NLP', percentage: 21 }, - ], - totalMembers: 132450, - totalSkills: 210, - }, - { - color: '#2C4A6E', - icon: IconOutline.ServerIcon, - id: 'e50b1794-e08d-4dc3-a4b1-6b5213c7da8e', - name: 'Databases & Data Warehousing', - officialName: 'Databases and Data Warehousing', - size: 5, - skillsBreakdown: [ - { name: 'PostgreSQL', percentage: 34 }, - { name: 'Snowflake', percentage: 28 }, - { name: 'Redshift', percentage: 20 }, - ], - totalMembers: 121800, - totalSkills: 118, - }, - { - color: '#3D5C5C', - icon: IconOutline.PencilAltIcon, - id: '3eb5163c-cd3f-4c7d-b059-95c901bc2066', - name: 'UX Design & Multimedia', - officialName: 'User Experience Design and Multimedia', - size: 4.6, - skillsBreakdown: [ - { name: 'Figma', percentage: 42 }, - { name: 'UX Research', percentage: 24 }, - { name: 'Motion', percentage: 16 }, - ], - totalMembers: 98600, - totalSkills: 76, - }, - { - color: '#6B7C4A', - icon: IconOutline.CalculatorIcon, - id: 'b3c8970d-79e8-4f97-a84e-841aebaa890f', - name: 'Mathematics & Statistics', - officialName: 'Mathematics and Statistics', - size: 4.5, - skillsBreakdown: [ - { name: 'Statistics', percentage: 38 }, - { name: 'Linear Algebra', percentage: 27 }, - { name: 'R', percentage: 19 }, - ], - totalMembers: 87400, - totalSkills: 64, - }, - { - color: '#2D5A8A', - icon: IconOutline.CubeTransparentIcon, - id: '35e9e3c6-3480-4fdb-9f77-91e667923a01', - name: 'Virtualization', - officialName: 'Virtualization', - size: 4.4, - skillsBreakdown: [ - { name: 'VMware', percentage: 36 }, - { name: 'Hyper-V', percentage: 28 }, - { name: 'KVM', percentage: 20 }, - ], - totalMembers: 76210, - totalSkills: 41, - }, - { - color: '#5A8A8A', - icon: IconOutline.MapIcon, - id: '6b9717ef-9520-4507-9039-2acedbec002d', - name: 'Geospatial Information Systems', - officialName: 'Geospatial Information Systems (GIS)', - size: 4, - skillsBreakdown: [ - { name: 'ArcGIS', percentage: 40 }, - { name: 'QGIS', percentage: 28 }, - { name: 'GeoJSON', percentage: 18 }, - ], - totalMembers: 54120, - totalSkills: 52, - }, - { - color: '#4EC4C4', - icon: IconOutline.DesktopComputerIcon, - id: '831ed28d-c20f-40c8-a348-d1d3739e9046', - name: 'Hardware & Systems Administration', - officialName: 'Hardware and Systems Administration', - size: 3.9, - skillsBreakdown: [ - { name: 'Linux Admin', percentage: 36 }, - { name: 'Networking', percentage: 27 }, - { name: 'Hardware', percentage: 21 }, - ], - totalMembers: 49880, - totalSkills: 58, - }, - { - color: '#2C4A6E', - icon: IconOutline.ClipboardCheckIcon, - id: 'c5f83f60-4dcf-4305-b55e-b38fe5afec60', - name: 'Software Testing & QA', - officialName: 'Software Testing and Quality Assurance', - size: 3.8, - skillsBreakdown: [ - { name: 'Selenium', percentage: 34 }, - { name: 'Cypress', percentage: 28 }, - { name: 'JMeter', percentage: 20 }, - ], - totalMembers: 46750, - totalSkills: 72, - }, - { - color: '#5A8A9A', - icon: IconOutline.DatabaseIcon, - id: '38fadd80-8721-4ce0-9387-cb6ad3ce48da', - name: 'Database Management', - officialName: 'Database Management', - size: 3.7, - skillsBreakdown: [ - { name: 'MySQL', percentage: 38 }, - { name: 'Oracle', percentage: 26 }, - { name: 'MongoDB', percentage: 20 }, - ], - totalMembers: 43210, - totalSkills: 81, - }, - { - color: '#4A9A9A', - icon: IconOutline.DeviceMobileIcon, - id: '0ae22576-48ed-4ffd-9319-058b6fd80675', - name: 'Mobile App Development', - officialName: 'Mobile App Development', - size: 3.5, - skillsBreakdown: [ - { name: 'Swift', percentage: 32 }, - { name: 'Kotlin', percentage: 30 }, - { name: 'React Native', percentage: 22 }, - ], - totalMembers: 38940, - totalSkills: 96, - }, - { - color: '#3D6A8A', - icon: IconOutline.ShareIcon, - id: 'f1daa100-b63b-45c1-a638-90fbfc817200', - name: 'Blockchain', - officialName: 'Blockchain', - size: 3.3, - skillsBreakdown: [ - { name: 'Solidity', percentage: 40 }, - { name: 'Ethereum', percentage: 28 }, - { name: 'Web3', percentage: 18 }, - ], - totalMembers: 27650, - totalSkills: 44, - }, - { - color: '#5EB8B0', - icon: IconOutline.WifiIcon, - id: 'e4a51b10-ecba-46eb-89e2-5908bb324a8c', - name: 'IoT (Internet of Things)', - officialName: 'IoT (Internet of Things)', - size: 3.2, - skillsBreakdown: [ - { name: 'MQTT', percentage: 34 }, - { name: 'Embedded C', percentage: 28 }, - { name: 'Arduino', percentage: 22 }, - ], - totalMembers: 24180, - totalSkills: 39, - }, - { - color: '#3D5A6E', - icon: IconOutline.ClipboardListIcon, - id: '07a0abe3-2791-4068-b5b5-be48fefa3551', - name: 'Project Management', - officialName: 'Project Management', - size: 3.1, - skillsBreakdown: [ - { name: 'Jira', percentage: 36 }, - { name: 'PMP', percentage: 26 }, - { name: 'Kanban', percentage: 22 }, - ], - totalMembers: 21890, - totalSkills: 27, - }, - { - color: '#4A6A7A', - icon: IconOutline.CodeIcon, - id: '1f5ed3e8-8d22-44ea-b75d-ea85147a04da', - name: 'Scripting & Automation', - officialName: 'Scripting and Automation', - size: 3, - skillsBreakdown: [ - { name: 'Bash', percentage: 36 }, - { name: 'Python', percentage: 32 }, - { name: 'PowerShell', percentage: 18 }, - ], - totalMembers: 19640, - totalSkills: 33, - }, -] diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/mock/skill-members.mock.ts b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/mock/skill-members.mock.ts deleted file mode 100644 index 9001c5cf1..000000000 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/mock/skill-members.mock.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { - PROGRAMMING_CATEGORY_ID, - SkillMemberMock, - SKILL_CATEGORIES, -} from './skill-categories.mock' - -const COUNTRIES: Array<{ code: string; name: string }> = [ - { code: 'IN', name: 'India' }, - { code: 'US', name: 'USA' }, - { code: 'CN', name: 'China' }, - { code: 'GB', name: 'UK' }, - { code: 'UA', name: 'Ukraine' }, - { code: 'CA', name: 'Canada' }, - { code: 'BR', name: 'Brazil' }, - { code: 'DE', name: 'Germany' }, - { code: 'JP', name: 'Japan' }, - { code: 'AU', name: 'Australia' }, -] - -const HANDLES = [ - 'skywalker', 'bytecraft', 'codecat', 'pixelhawk', 'algomind', - 'devnova', 'stackpilot', 'nimbusdev', 'qubitron', 'hashlane', - 'loopsmith', 'gridfox', 'nullwave', 'bitforge', 'cloudnest', - 'syntaxio', 'datapath', 'kernelfox', 'vectorly', 'modulin', -] - -const FIRST_NAMES = [ - 'Alex', 'Jordan', 'Priya', 'Wei', 'Sofia', 'Noah', 'Amina', 'Lucas', 'Mei', 'Omar', -] - -const LAST_NAMES = [ - 'Chen', 'Patel', 'Nguyen', 'Garcia', 'Khan', 'Silva', 'Ivanov', 'Kim', 'Brown', 'Rossi', -] - -const RATINGS = [780, 950, 1100, 1350, 1480, 1600, 1800, 1900, 2000, 2100, 2200, 2300, 2400] - -const PROGRAMMING_TOP_MEMBERS: SkillMemberMock[] = [ - { - countryCode: 'IN', - countryName: 'India', - handle: 'billzedison', - name: 'Honghan W', - rating: 2000, - wins: 376, - }, - { - countryCode: 'US', - countryName: 'USA', - handle: 'Ghostar', - name: 'Justin G', - rating: 1900, - wins: 322, - }, - { - countryCode: 'IN', - countryName: 'India', - handle: 'stevenfrog', - name: 'Steven', - rating: 2100, - wins: 280, - }, - { - countryCode: 'CN', - countryName: 'China', - handle: 'ergolite', - name: 'Michael P', - rating: 2200, - wins: 262, - }, - { - countryCode: 'IN', - countryName: 'India', - handle: 'jiangliwu', - name: 'Jiang L', - rating: 950, - wins: 210, - }, - { - countryCode: 'GB', - countryName: 'UK', - handle: 'diazx', - name: 'DAT N', - rating: 2300, - wins: 200, - }, - { - countryCode: 'US', - countryName: 'USA', - handle: 'Standlove', - name: 'GuanZhao I', - rating: 1800, - wins: 188, - }, - { - countryCode: 'IN', - countryName: 'India', - handle: 'soso0574', - name: 'Jianchang S', - rating: 1600, - wins: 176, - }, - { - countryCode: 'IN', - countryName: 'India', - handle: 'vasilica.olaru', - name: 'vasilica.olaru', - rating: 2400, - wins: 132, - }, - { - countryCode: 'IN', - countryName: 'India', - handle: 'ngoctay', - name: 'Minh Ngoc P', - rating: 780, - wins: 90, - }, -] - -function hashString(value: string): number { - let hash = 0 - for (let index = 0; index < value.length; index += 1) { - hash = ((hash * 31) + value.charCodeAt(index)) % 2147483647 - } - - return Math.abs(hash) -} - -function buildGeneratedMembers(categoryId: string, startWins: number): SkillMemberMock[] { - const members: SkillMemberMock[] = [] - - for (let index = 0; index < 90; index += 1) { - const seed = hashString(`${categoryId}-${index}`) - const country = COUNTRIES[seed % COUNTRIES.length] - const firstName = FIRST_NAMES[seed % FIRST_NAMES.length] - const lastName = LAST_NAMES[hashString(`${categoryId}-last-${index}`) % LAST_NAMES.length] - const handleBase = HANDLES[hashString(`${categoryId}-handle-${index}`) % HANDLES.length] - - members.push({ - countryCode: country.code, - countryName: country.name, - handle: `${handleBase}${index + 1}`, - name: `${firstName} ${lastName.charAt(0)}`, - rating: RATINGS[seed % RATINGS.length], - wins: Math.max(1, startWins - index), - }) - } - - return members -} - -function buildMembersForCategory(categoryId: string): SkillMemberMock[] { - if (categoryId === PROGRAMMING_CATEGORY_ID) { - return [ - ...PROGRAMMING_TOP_MEMBERS, - ...buildGeneratedMembers(categoryId, 89), - ] - } - - const seed = hashString(categoryId) - const startWins = 120 + (seed % 80) - - return buildGeneratedMembers(categoryId, startWins) - .slice(0, 100) - .sort((left, right) => right.wins - left.wins) -} - -export const SKILL_MEMBERS_BY_CATEGORY: Record = Object.fromEntries( - SKILL_CATEGORIES.map(category => [ - category.id, - buildMembersForCategory(category.id), - ]), -) - -export function getMembersForCategory(categoryId: string): SkillMemberMock[] { - return SKILL_MEMBERS_BY_CATEGORY[categoryId] || [] -} - -export function getTopMemberForCategory(categoryId: string): SkillMemberMock | undefined { - if (categoryId === PROGRAMMING_CATEGORY_ID) { - return { - countryCode: 'IN', - countryName: 'India', - handle: 'banerjeesourish', - name: 'Sourish Banerjee', - rating: 1400, - wins: 1768, - } - } - - return getMembersForCategory(categoryId)[0] -} From ea203ffef76ad8b9fda978c752be7c8b27f10016 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 24 Aug 2026 15:11:24 +0530 Subject: [PATCH 2/2] Fix css --- .../SkillBubblesChart.module.scss | 29 ++++++++++++++----- .../SkillStatisticsPage/SkillBubblesChart.tsx | 13 +++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.module.scss b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.module.scss index da0416245..221cac070 100644 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.module.scss +++ b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.module.scss @@ -17,25 +17,19 @@ align-items: center; border: 0; border-radius: 50%; + box-sizing: border-box; color: #fff; cursor: pointer; display: flex; - flex-direction: column; justify-content: center; overflow: hidden; - padding: 8px; + padding: 0; position: absolute; text-align: center; transform: translate(-50%, -50%); transition: box-shadow 160ms ease, transform 160ms ease; z-index: 1; - svg { - color: #fff; - flex: 0 0 auto; - margin-bottom: 4px; - } - &:hover, &:focus-visible, &.hovered { @@ -54,12 +48,31 @@ z-index: 4; } +.bubbleInner { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + width: 80%; + + svg { + color: #fff; + flex: 0 0 auto; + margin-bottom: 4px; + } +} + .label { display: -webkit-box; font-family: 'Nunito Sans', sans-serif; font-weight: 700; line-height: 1.15; + max-width: 100%; overflow: hidden; + overflow-wrap: anywhere; + width: 100%; + word-break: break-word; -webkit-box-orient: vertical; -webkit-line-clamp: 3; } diff --git a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx index 09f395788..040f36424 100644 --- a/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx +++ b/src/apps/customer-portal/src/pages/skill-statistics/SkillStatisticsPage/SkillBubblesChart.tsx @@ -37,8 +37,8 @@ const POPOVER_GAP = 12 const POPOVER_ESTIMATED_HEIGHT = 340 const POPOVER_WIDTH = 320 const VIEW_PAD = 8 -const MIN_BUBBLE_FONT_SIZE = 12 -const MAX_BUBBLE_FONT_SIZE = 20 +const MIN_BUBBLE_FONT_SIZE = 10 +const MAX_BUBBLE_FONT_SIZE = 16 type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right' @@ -258,7 +258,8 @@ const SkillBubblesChart: FC = props => { minPackedRadius, maxPackedRadius, ) - const iconSize = Math.max(14, Math.min(28, circle.r / 4.6)) + const innerSize = circle.r * 1.16 + const iconSize = Math.max(12, Math.min(22, innerSize / 5.5)) return ( ) })}