{hasSelectors ? (
@@ -32,7 +36,8 @@ export const TableSkeleton = ({
))}
- |
+
+ {hasAnyActions && | }
{hasCopyButton && | }
@@ -49,14 +54,17 @@ export const TableSkeleton = ({
))}
-
-
- |
+ {hasIcon && (
+
+
+ |
+ )}
+
{hasCopyButton && (
>(
}: TableProps) => {
const hasActions = !!renderActions;
- const isAllSelected = selectedItems?.length === items.length;
+ const isAllSelected = selectedItems?.length === items.length && items.length > 0;
const selectedItemsIds = selectedItems?.map(({ id }) => id) || [];
+ const allSelected = selectedItems?.length ?? 0;
+ const isIntermediate = allSelected > 0 && allSelected < items.length;
+
const onSelectAllItems = () => {
onSelectItems?.(isAllSelected ? [] : items.map((item) => ({ id: item.id, title: item.title })));
};
@@ -86,7 +89,11 @@ export const Table = >(
{selectedItems && (
|
-
+
|
)}
{renderTableHeader()}
diff --git a/src/shared/ui/Tabs/Tabs.module.css b/src/shared/ui/Tabs/Tabs.module.css
index 9fd401bc5..d539ead1a 100644
--- a/src/shared/ui/Tabs/Tabs.module.css
+++ b/src/shared/ui/Tabs/Tabs.module.css
@@ -1,31 +1,48 @@
.tab-container {
position: relative;
+ padding-bottom: 20px;
}
.tab-list {
+ align-self: flex-start;
+ padding: 10px;
overflow-x: auto;
+ border-radius: 18px;
list-style-type: none;
scrollbar-width: none;
}
.tab-item {
flex-shrink: 0;
- padding-bottom: 8px;
+ padding: 10px;
+ border-radius: 8px;
cursor: pointer;
}
-.tab-item.active {
- position: relative;
+.tab-item:hover{
+ color: var(--border-image-hover);
+ transition: all .3s ease-in-out;
+ cursor: pointer;
}
-.tab-item.active::after {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 2px;
- border-radius: 18px;
- background: var(--background-primary);
- transition: all .3s ease-in-out;
- content: '';
-}
\ No newline at end of file
+.tab-item.default:hover,
+.tab-item.default.active {
+ background-color: var(--background-app);
+}
+
+.tab-item.gray:hover,
+.tab-item.gray.active {
+ background-color: var(--background-block);
+}
+
+.tab-item:hover > p {
+ color:var(--border-image-hover);
+}
+
+.default {
+ background-color: var(--background-block);
+}
+
+.gray {
+ background-color: var(--background-light-hover);
+}
diff --git a/src/shared/ui/Tabs/Tabs.skeleton.tsx b/src/shared/ui/Tabs/Tabs.skeleton.tsx
index f0b687022..26084eb58 100644
--- a/src/shared/ui/Tabs/Tabs.skeleton.tsx
+++ b/src/shared/ui/Tabs/Tabs.skeleton.tsx
@@ -12,14 +12,13 @@ interface TabsSkeleton {
export const TabsSkeleton = ({ tabs }: TabsSkeleton) => {
return (
-
+
{tabs?.map((tab) => (
))}
-
);
};
diff --git a/src/shared/ui/Tabs/Tabs.test.tsx b/src/shared/ui/Tabs/Tabs.test.tsx
index 12f2976e8..432155280 100644
--- a/src/shared/ui/Tabs/Tabs.test.tsx
+++ b/src/shared/ui/Tabs/Tabs.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { Tab, Tabs, TabsProps } from './Tabs';
@@ -32,29 +32,13 @@ const renderComponent = (props = {}) => {
};
describe('Tabs Component', () => {
- test('sets the active tab and line position correctly on initial render', () => {
- renderComponent();
-
- const tab1 = screen.getByTestId('Tabs_Item_tab-1');
- const lineRef = screen.getByTestId('Tabs_Line');
-
- expect(lineRef).toHaveStyle(`left: ${tab1.offsetLeft}px`);
- expect(lineRef).toHaveStyle(`width: ${tab1.offsetWidth}px`);
- });
-
test('updates line position and active tab on tab click', async () => {
renderComponent();
const tab2 = screen.getByTestId('Tabs_Item_tab-2');
- const lineRef = screen.getByTestId('Tabs_Line');
fireEvent.click(tab2);
- await waitFor(() => {
- expect(lineRef).toHaveStyle(`left: ${tab2.offsetLeft}px`);
- expect(lineRef).toHaveStyle(`width: ${tab2.offsetWidth}px`);
- });
-
expect(setActiveTab).toHaveBeenCalledWith(mockTabs[1]);
});
diff --git a/src/shared/ui/Tabs/Tabs.tsx b/src/shared/ui/Tabs/Tabs.tsx
index 03ccc402e..ab5aeea52 100644
--- a/src/shared/ui/Tabs/Tabs.tsx
+++ b/src/shared/ui/Tabs/Tabs.tsx
@@ -1,5 +1,5 @@
import classNames from 'classnames';
-import { Dispatch, Key, SetStateAction, useEffect, useRef } from 'react';
+import { Dispatch, Key, SetStateAction } from 'react';
import { useNavigate } from 'react-router-dom';
import { Flex } from '@/shared/ui/Flex';
@@ -14,46 +14,29 @@ export interface Tab {
Component: () => JSX.Element;
}
+type TabColor = 'default' | 'gray';
+
export interface TabsProps {
tabs: Tab[];
activeTab: Tab;
+ color?: TabColor;
setActiveTab: Dispatch>>;
}
-export const Tabs = ({ tabs, activeTab, setActiveTab }: TabsProps) => {
- const lineRef = useRef(null);
+export const Tabs = ({ tabs, activeTab, setActiveTab, color = 'default' }: TabsProps) => {
const navigate = useNavigate();
- const onTabToggle = (e: React.MouseEvent, tab: Tab) => {
- const tabElement = e.target as HTMLLIElement;
- const tabRect = tabElement.offsetLeft;
-
+ const onTabToggle = (tab: Tab) => {
setActiveTab(tab);
navigate(`#${tab.id}`, { replace: true });
-
- if (lineRef.current) {
- lineRef.current.style.width = tabElement.offsetWidth + 'px';
- lineRef.current.style.left = `${tabRect}px`;
- }
};
- useEffect(() => {
- const tabElement = document.querySelector(
- `.${styles['tab-item']}.${styles.active}`,
- ) as HTMLLIElement | null;
- if (tabElement && lineRef.current) {
- const tabRect = tabElement.offsetLeft;
- lineRef.current.style.width = `${tabElement.offsetWidth}px`;
- lineRef.current.style.left = `${tabRect}px`;
- }
- }, [activeTab]);
-
return (
@@ -61,19 +44,20 @@ export const Tabs = ({ tabs, activeTab, setActiveTab }: TabsProps) => {
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
onTabToggle(e, tab)}
+ className={classNames(styles['tab-item'], styles[color], {
+ [styles.active]: activeTab.id === tab.id,
+ })}
+ onClick={() => onTabToggle(tab)}
role="tab"
tabIndex={0}
data-testid={`Tabs_Item_${tab.id}`}
>
-
+
{tab.label} {(tab.count ?? 0) > 0 && `(${tab.count})`}
))}
-
);
};
diff --git a/src/widgets/Collection/ui/AdditionalInfo/AdditionalInfo.tsx b/src/widgets/Collection/ui/AdditionalInfo/AdditionalInfo.tsx
index 215fe2cfb..3ae3d8782 100644
--- a/src/widgets/Collection/ui/AdditionalInfo/AdditionalInfo.tsx
+++ b/src/widgets/Collection/ui/AdditionalInfo/AdditionalInfo.tsx
@@ -20,10 +20,11 @@ import { SpecializationsList } from '@/entities/specialization';
import styles from './AdditionalInfo.module.css';
-interface AdditionalInfoProps extends Pick<
- Collection,
- 'specializations' | 'isFree' | 'company' | 'questionsCount' | 'createdBy' | 'keywords'
-> {
+interface AdditionalInfoProps
+ extends Pick<
+ Collection,
+ 'specializations' | 'isFree' | 'company' | 'questionsCount' | 'createdBy' | 'keywords'
+ > {
showAuthor?: boolean;
className?: string;
media?: Media | undefined;
diff --git a/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx b/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx
index cd9c2b54f..4eca4e37c 100644
--- a/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx
+++ b/src/widgets/Collection/ui/CollectionHeader/CollectionHeader.tsx
@@ -11,10 +11,8 @@ import { Collection } from '@/entities/collection';
import styles from './CollectionHeader.module.css';
-interface CollectionHeaderProps extends Pick<
- Collection,
- 'title' | 'description' | 'imageSrc' | 'company'
-> {
+interface CollectionHeaderProps
+ extends Pick {
renderDrawer: () => ReactNode;
}
diff --git a/src/widgets/Landing/Avos/ui/AvosPromo/AvosPromo.skeleton.tsx b/src/widgets/Landing/Avos/ui/AvosPromo/AvosPromo.skeleton.tsx
index 5322c4a00..aff54807e 100644
--- a/src/widgets/Landing/Avos/ui/AvosPromo/AvosPromo.skeleton.tsx
+++ b/src/widgets/Landing/Avos/ui/AvosPromo/AvosPromo.skeleton.tsx
@@ -39,7 +39,7 @@ export const AvosPromoSkeleton = () => {
/>
-
+
);
diff --git a/src/widgets/analytics/AnalyticPageTemplate/index.ts b/src/widgets/analytics/AnalyticPageTemplate/index.ts
index 9d2b05e8d..cbe6833b0 100644
--- a/src/widgets/analytics/AnalyticPageTemplate/index.ts
+++ b/src/widgets/analytics/AnalyticPageTemplate/index.ts
@@ -2,3 +2,4 @@ export { AnalyticPageTemplate } from './ui/AnalyticPageTemplate/AnalyticPageTemp
export { AnalyticPageTemplateMobileList } from './ui/AnalyticPageTemplateMobileList/AnalyticPageTemplateMobileList';
export { useAnalyticFilters } from './model/hooks/useAnalyticFilters';
export type { AnalyticPageTemplateMobileListItem } from './model/types/types';
+export { AnalyticPageTemplateSkeleton } from './ui/AnalyticPageTemplate/AnalyticPageTemplate.skeleton';
diff --git a/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplate/AnalyticPageTemplate.skeleton.tsx b/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplate/AnalyticPageTemplate.skeleton.tsx
new file mode 100644
index 000000000..61c217309
--- /dev/null
+++ b/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplate/AnalyticPageTemplate.skeleton.tsx
@@ -0,0 +1,55 @@
+import { useScreenSize } from '@/shared/libs';
+import { CardSkeleton } from '@/shared/ui/Card';
+import { Flex } from '@/shared/ui/Flex';
+import { Skeleton } from '@/shared/ui/Skeleton';
+import { TablePaginationSkeleton } from '@/shared/ui/TablePagination';
+import { TextSkeleton } from '@/shared/ui/Text';
+
+import { AnalyticPageTemplateProps } from './AnalyticPageTemplate';
+
+interface AnalyticPageTemplateSkeletonProps extends AnalyticPageTemplateProps {
+ showSkillFilter?: boolean;
+}
+
+export const AnalyticPageTemplateSkeleton = ({
+ list,
+ table,
+ filters,
+ showSkillFilter = false,
+}: AnalyticPageTemplateSkeletonProps) => {
+ const { isMobile } = useScreenSize();
+
+ const { page, total, limit } = filters;
+
+ const showPagination = page && total && limit && total > limit;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {showSkillFilter && }
+
+
+ {isMobile ? list : table}
+
+ {showPagination && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplate/AnalyticPageTemplate.tsx b/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplate/AnalyticPageTemplate.tsx
index 15d77f292..cc917d9e2 100644
--- a/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplate/AnalyticPageTemplate.tsx
+++ b/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplate/AnalyticPageTemplate.tsx
@@ -29,7 +29,7 @@ interface AnalyticPageTemplateFilters {
onResetFilters?: () => void;
}
-interface AnalyticPageTemplateProps {
+export interface AnalyticPageTemplateProps {
title: string;
tooltip?: string;
list: ReactNode;
diff --git a/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplateMobileList/AnalyticPageTemplateMobileList.skeleton.tsx b/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplateMobileList/AnalyticPageTemplateMobileList.skeleton.tsx
new file mode 100644
index 000000000..426013fad
--- /dev/null
+++ b/src/widgets/analytics/AnalyticPageTemplate/ui/AnalyticPageTemplateMobileList/AnalyticPageTemplateMobileList.skeleton.tsx
@@ -0,0 +1,30 @@
+import { Flex } from '@/shared/ui/Flex';
+import { TextSkeleton } from '@/shared/ui/Text';
+
+import styles from './AnalyticPageTemplateMobileList.module.css';
+
+export const AnalyticPageTemplateMobileListSkeleton = () => {
+ return (
+
+ {[...Array(10)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx b/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx
index adab4f37d..19f58ee6b 100644
--- a/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx
+++ b/src/widgets/analytics/PopularSkillsWidget/ui/PopularSkillsWidget/PopularSkillsWidget.tsx
@@ -36,14 +36,16 @@ const PopularSkillsWidget = () => {
isActionPositionBottom
>
- {data?.data.slice(0, 3).map((item) => (
-
- ))}
+ {data?.data
+ .slice(0, 3)
+ .map((item) => (
+
+ ))}
);
diff --git a/src/widgets/analytics/UsersRatingWidget/index.ts b/src/widgets/analytics/UsersRatingWidget/index.ts
new file mode 100644
index 000000000..ba4b173a8
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/index.ts
@@ -0,0 +1,2 @@
+export { UsersRatingWidget } from './ui/UsersRatingWidget/UsersRatingWidget';
+export { UsersRatingWidgetSkeleton } from './ui/UsersRatingWidget/UsersRatingWidget.skeleton';
diff --git a/src/widgets/analytics/UsersRatingWidget/model/constants/index.ts b/src/widgets/analytics/UsersRatingWidget/model/constants/index.ts
new file mode 100644
index 000000000..973babb72
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/model/constants/index.ts
@@ -0,0 +1,9 @@
+import { firstPlaceIcon, secondPlaceIcon, thirdPlaceIcon } from '@/shared/assets';
+
+export const AVATAR_RADII = { 1: 56, 2: 44.5, 3: 39 };
+export const PLACE_ICONS = {
+ 1: firstPlaceIcon,
+ 2: secondPlaceIcon,
+ 3: thirdPlaceIcon,
+};
+export const TOP_PLACES_COUNT = 3;
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.module.css b/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.module.css
new file mode 100644
index 000000000..03b362517
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.module.css
@@ -0,0 +1,27 @@
+.avatar-with-rating {
+ position: relative;
+ display: inline-block;
+}
+
+.avatar-image {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ border-style: solid;
+ border-color: var(--color-white-900);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ object-fit: cover;
+}
+
+.empty-circle {
+ stroke: var(--color-purple-100);
+ fill: transparent;
+}
+
+.filled-circle {
+ stroke: var(--color-purple-700);
+ stroke-linecap: round;
+ fill: transparent;
+ transition: 'stroke-dashoffset 0.3s'
+}
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.skeleton.tsx b/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.skeleton.tsx
new file mode 100644
index 000000000..0697c032d
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.skeleton.tsx
@@ -0,0 +1,23 @@
+import classNames from 'classnames';
+
+import { AvatarSkeleton } from '@/shared/ui/Avatar';
+
+import styles from './AvatarWithRating.module.css';
+
+interface AvatarWithRatingProps {
+ radius: number;
+ className?: string;
+}
+
+export const AvatarWithRatingSkeleton = ({ radius, className }: AvatarWithRatingProps) => {
+ const size = radius * 2;
+
+ return (
+
+ );
+};
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.tsx b/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.tsx
new file mode 100644
index 000000000..0d46ef3ab
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/AvatarWithRating/AvatarWithRating.tsx
@@ -0,0 +1,62 @@
+import classNames from 'classnames';
+
+import { Avatar } from '@/shared/ui/Avatar';
+
+import styles from './AvatarWithRating.module.css';
+
+interface AvatarWithRatingProps {
+ avatarUrl: string;
+ score: number;
+ radius: number;
+ maxRating: number;
+ className?: string;
+}
+
+export const AvatarWithRating = ({
+ avatarUrl,
+ score,
+ maxRating,
+ radius,
+ className,
+}: AvatarWithRatingProps) => {
+ const size = radius * 2;
+ const strokeWidth = radius * 0.12;
+ const circleRadius = radius - strokeWidth / 2;
+
+ const circumference = 2 * Math.PI * circleRadius;
+ const progress = Math.min(1, Math.max(0, score / maxRating));
+ const offset = circumference * (1 - progress);
+
+ return (
+
+ );
+};
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.module.css b/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.module.css
new file mode 100644
index 000000000..acbda93d8
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.module.css
@@ -0,0 +1,14 @@
+.user-rating {
+ padding: 12px 4px;
+ width: 100%;
+}
+
+.user-name {
+ max-width: 87px;
+}
+
+@media (width < 480px) {
+ .user-name {
+ max-width: 58px;
+ }
+}
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.skeleton.tsx b/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.skeleton.tsx
new file mode 100644
index 000000000..e630b70f4
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.skeleton.tsx
@@ -0,0 +1,33 @@
+import { useScreenSize } from '@/shared/libs';
+import { CardSkeleton } from '@/shared/ui/Card';
+import { Flex } from '@/shared/ui/Flex';
+import { TextSkeleton } from '@/shared/ui/Text';
+
+import { AVATAR_RADII } from '../../model/constants';
+import { AvatarWithRatingSkeleton } from '../AvatarWithRating/AvatarWithRating.skeleton';
+
+import styles from './UserRatingItem.module.css';
+
+interface UserRatingItemPropsSkeleton {
+ place: 1 | 2 | 3;
+}
+
+export const UserRatingItemSkeleton = ({ place }: UserRatingItemPropsSkeleton) => {
+ const { isMobileS } = useScreenSize();
+ const itemWidth = isMobileS ? AVATAR_RADII[1] * 1.5 : AVATAR_RADII[1] * 2;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.tsx b/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.tsx
new file mode 100644
index 000000000..0a1366a0b
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/UserRatingItem/UserRatingItem.tsx
@@ -0,0 +1,60 @@
+import { useRef } from 'react';
+
+import { useScreenSize, useTruncation } from '@/shared/libs';
+import { Card } from '@/shared/ui/Card';
+import { Flex } from '@/shared/ui/Flex';
+import { Text } from '@/shared/ui/Text';
+import { Tooltip } from '@/shared/ui/Tooltip';
+
+import type { UserRating } from '@/entities/user';
+
+import { AVATAR_RADII, PLACE_ICONS } from '../../model/constants';
+import { AvatarWithRating } from '../AvatarWithRating/AvatarWithRating';
+
+import styles from './UserRatingItem.module.css';
+
+interface UserRatingItemProps {
+ userRating: UserRating;
+ place: 1 | 2 | 3;
+ questionsCount: number;
+}
+
+export const UserRatingItem = ({ userRating, place, questionsCount }: UserRatingItemProps) => {
+ const { isMobileS } = useScreenSize();
+ const itemWidth = isMobileS ? AVATAR_RADII[1] * 1.5 : AVATAR_RADII[1] * 2;
+ const nameRef = useRef(null);
+ const isTruncated = useTruncation(nameRef, 'row');
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {userRating.username}
+
+
+
+
+ {userRating.ratingScore}/{questionsCount}
+
+
+
+
+ );
+};
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.module.css b/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.module.css
new file mode 100644
index 000000000..cfc49d39f
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.module.css
@@ -0,0 +1,23 @@
+.card {
+ gap: 20px;
+ width: 100%;
+ max-width: 455px;
+}
+
+.wrapper {
+ width: 112px;
+}
+
+@media (width < 1280px) {
+ .card {
+ padding: 16px;
+ max-width: 100%;
+ }
+}
+
+@media (width < 768px) {
+ .card {
+ padding: 20px 10px;
+ max-width: 100%;
+ }
+}
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.skeleton.tsx b/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.skeleton.tsx
new file mode 100644
index 000000000..66b86bc30
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.skeleton.tsx
@@ -0,0 +1,23 @@
+import { CardSkeleton } from '@/shared/ui/Card';
+import { Flex } from '@/shared/ui/Flex';
+
+import { UserRatingItemSkeleton } from '../UserRatingItem/UserRatingItem.skeleton';
+
+import styles from './UsersRatingWidget.module.css';
+
+export const UsersRatingWidgetSkeleton = () => {
+ return (
+
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+ );
+};
diff --git a/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.tsx b/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.tsx
new file mode 100644
index 000000000..b661f7481
--- /dev/null
+++ b/src/widgets/analytics/UsersRatingWidget/ui/UsersRatingWidget/UsersRatingWidget.tsx
@@ -0,0 +1,52 @@
+import { useTranslation } from 'react-i18next';
+
+import { i18Namespace, Analytics } from '@/shared/config';
+import { useAppSelector } from '@/shared/libs';
+import { Card } from '@/shared/ui/Card';
+import { Flex } from '@/shared/ui/Flex';
+import { Text } from '@/shared/ui/Text';
+
+import { getSpecializationId } from '@/entities/profile';
+import { useGetUsersRatingBySpecializationQuery } from '@/entities/user';
+
+import { TOP_PLACES_COUNT } from '../../model/constants';
+import { UserRatingItem } from '../UserRatingItem/UserRatingItem';
+
+import styles from './UsersRatingWidget.module.css';
+import { UsersRatingWidgetSkeleton } from './UsersRatingWidget.skeleton';
+
+export const UsersRatingWidget = () => {
+ const { t } = useTranslation(i18Namespace.analytics);
+ const specializationId = String(useAppSelector(getSpecializationId));
+ const { data, isLoading } = useGetUsersRatingBySpecializationQuery(specializationId);
+ const topUsers = data?.users.slice(0, TOP_PLACES_COUNT) || [];
+ const topUsersIsEmpty = topUsers.length === 0;
+ const specialization = data?.specialization;
+ const questionsCount = data?.questionsCount ?? 0;
+
+ if (isLoading) return ;
+
+ return (
+
+ {!topUsersIsEmpty ? (
+
+ {topUsers.map((data, i) => (
+
+ ))}
+
+ ) : (
+ {t(Analytics.TOP_USERS_NO_DATA_WIDGET)}
+ )}
+
+ );
+};
diff --git a/src/widgets/topic/TopicsTable/ui/TopicsTable.tsx b/src/widgets/topic/TopicsTable/ui/TopicsTable.tsx
index 505cb1f6b..8ad2426c8 100644
--- a/src/widgets/topic/TopicsTable/ui/TopicsTable.tsx
+++ b/src/widgets/topic/TopicsTable/ui/TopicsTable.tsx
@@ -1,6 +1,5 @@
import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
-import { Link } from 'react-router-dom';
+import { Link, useNavigate } from 'react-router-dom';
import { i18Namespace, Topics, Translation, ROUTES } from '@/shared/config';
import { SelectedAdminEntities, formatDate, route } from '@/shared/libs';
@@ -14,17 +13,25 @@ import { Text } from '@/shared/ui/Text';
import { Topic } from '@/entities/topic';
+import { DeleteTopicButton } from '@/features/topics/deleteTopic';
+
import styles from './TopicsTable.module.css';
interface TopicsTableProps {
topics?: Topic[];
selectedTopics?: SelectedAdminEntities;
onSelectTopics?: (ids: SelectedAdminEntities) => void;
+ onDeleteSuccess?: () => void;
}
-export const TopicsTable = ({ topics, selectedTopics, onSelectTopics }: TopicsTableProps) => {
- const navigate = useNavigate();
+export const TopicsTable = ({
+ topics,
+ selectedTopics,
+ onSelectTopics,
+ onDeleteSuccess,
+}: TopicsTableProps) => {
const { t } = useTranslation([i18Namespace.topic, i18Namespace.translation]);
+ const navigate = useNavigate();
const renderTableColumnWidth = () => {
const columnWidths = {
@@ -95,8 +102,29 @@ export const TopicsTable = ({ topics, selectedTopics, onSelectTopics }: TopicsTa
navigate(route(ROUTES.admin.topics.details.page, topic.id));
},
},
- // Добавить редактировать и удалить если нужно
+ {
+ icon: ,
+ title: t(Translation.EDIT, { ns: i18Namespace.translation }),
+ tooltip: {
+ color: 'red',
+ text: t(Translation.TOOLTIP_COLLECTION_DISABLED_INFO, { ns: i18Namespace.translation }),
+ },
+ disabled: topic.disabled,
+ onClick: () => {
+ navigate(route(ROUTES.admin.topics.details.page, topic.id));
+ },
+ },
+ {
+ renderComponent: () => (
+
+ ),
+ },
];
+
return (
@@ -124,10 +152,11 @@ export const TopicsTable = ({ topics, selectedTopics, onSelectTopics }: TopicsTa
items={topics}
renderTableHeader={renderTableHeader}
renderTableBody={renderTableBody}
+ renderActions={renderActions}
renderTableColumnWidths={renderTableColumnWidth}
selectedItems={selectedTopics}
onSelectItems={onSelectTopics}
- renderActions={renderActions}
+ hasCopyButton
/>
);
};
|