feat: init
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { ProfileIssuesPage } from "@/components/profile/profile-issues";
|
||||
|
||||
const ProfilePageHeader = {
|
||||
assigned: "Profile - Assigned",
|
||||
created: "Profile - Created",
|
||||
subscribed: "Profile - Subscribed",
|
||||
};
|
||||
|
||||
const ProfileIssuesTypePage = () => {
|
||||
const { profileViewId } = useParams() as { profileViewId: "assigned" | "subscribed" | "created" | undefined };
|
||||
|
||||
if (!profileViewId) return null;
|
||||
|
||||
const header = ProfilePageHeader[profileViewId];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={header} />
|
||||
<ProfileIssuesPage type={profileViewId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileIssuesTypePage;
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { DownloadActivityButton } from "@/components/profile/activity/download-button";
|
||||
import { WorkspaceActivityListPage } from "@/components/profile/activity/workspace-activity-list";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
|
||||
const PER_PAGE = 100;
|
||||
|
||||
const ProfileActivityPage = observer(() => {
|
||||
// states
|
||||
const [pageCount, setPageCount] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [resultsCount, setResultsCount] = useState(0);
|
||||
// router
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
//hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateTotalPages = (count: number) => setTotalPages(count);
|
||||
|
||||
const updateResultsCount = (count: number) => setResultsCount(count);
|
||||
|
||||
const handleLoadMore = () => setPageCount((prev) => prev + 1);
|
||||
|
||||
const activityPages: React.ReactNode[] = [];
|
||||
for (let i = 0; i < pageCount; i++)
|
||||
activityPages.push(
|
||||
<WorkspaceActivityListPage
|
||||
key={i}
|
||||
cursor={`${PER_PAGE}:${i}:0`}
|
||||
perPage={PER_PAGE}
|
||||
updateResultsCount={updateResultsCount}
|
||||
updateTotalPages={updateTotalPages}
|
||||
/>
|
||||
);
|
||||
|
||||
const canDownloadActivity = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Profile - Activity" />
|
||||
<div className="flex h-full w-full flex-col overflow-hidden py-5">
|
||||
<div className="flex items-center justify-between gap-2 px-5 md:px-9">
|
||||
<h3 className="text-lg font-medium">{t("profile.stats.recent_activity.title")}</h3>
|
||||
{canDownloadActivity && <DownloadActivityButton />}
|
||||
</div>
|
||||
<div className="vertical-scrollbar scrollbar-md flex h-full flex-col overflow-y-auto px-5 md:px-9">
|
||||
{activityPages}
|
||||
{pageCount < totalPages && resultsCount !== 0 && (
|
||||
<div className="flex w-full items-center justify-center text-xs">
|
||||
<Button variant="accent-primary" size="sm" onClick={handleLoadMore}>
|
||||
{t("common.load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ProfileActivityPage;
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
// ui
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ChevronDown, PanelRight } from "lucide-react";
|
||||
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { YourWorkIcon } from "@plane/propel/icons";
|
||||
import type { IUserProfileProjectSegregation } from "@plane/types";
|
||||
import { Breadcrumbs, Header, CustomMenu } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { ProfileIssuesFilter } from "@/components/profile/profile-issues-filter";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
|
||||
type TUserProfileHeader = {
|
||||
userProjectsData: IUserProfileProjectSegregation | undefined;
|
||||
type?: string | undefined;
|
||||
showProfileIssuesFilter?: boolean;
|
||||
};
|
||||
|
||||
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||
const { userProjectsData, type = undefined, showProfileIssuesFilter } = props;
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
|
||||
const { data: currentUser } = useUser();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const isAuthorized = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
if (!workspaceUserInfo) return null;
|
||||
|
||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||
|
||||
const userName = `${userProjectsData?.user_data?.first_name} ${userProjectsData?.user_data?.last_name}`;
|
||||
|
||||
const isCurrentUser = currentUser?.id === userId;
|
||||
|
||||
const breadcrumbLabel = isCurrentUser ? t("profile.page_label") : `${userName} ${t("profile.work")}`;
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
component={
|
||||
<BreadcrumbLink
|
||||
label={breadcrumbLabel}
|
||||
disableTooltip
|
||||
icon={<YourWorkIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div>
|
||||
<div className="flex gap-4 md:hidden">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-sm text-custom-text-200"
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1.5">
|
||||
<span className="flex flex-grow justify-center text-sm text-custom-text-200">{type}</span>
|
||||
<ChevronDown className="h-4 w-4 text-custom-text-400" />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
<></>
|
||||
{tabsList.map((tab) => (
|
||||
<CustomMenu.MenuItem
|
||||
className="flex items-center gap-2"
|
||||
key={tab.route}
|
||||
onClick={() => router.push(`/${workspaceSlug}/profile/${userId}/${tab.route}`)}
|
||||
>
|
||||
<span className="w-full text-custom-text-300">{t(tab.i18n_label)}</span>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<button
|
||||
className="block transition-all md:hidden"
|
||||
onClick={() => {
|
||||
toggleProfileSidebar();
|
||||
}}
|
||||
>
|
||||
<PanelRight
|
||||
className={cn(
|
||||
"block h-4 w-4 md:hidden",
|
||||
!profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { AppHeader } from "@/components/core/app-header";
|
||||
import { ContentWrapper } from "@/components/core/content-wrapper";
|
||||
import { ProfileSidebar } from "@/components/profile/sidebar";
|
||||
// constants
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// local components
|
||||
import { UserService } from "@/services/user.service";
|
||||
import { UserProfileHeader } from "./header";
|
||||
import { ProfileIssuesMobileHeader } from "./mobile-header";
|
||||
import { ProfileNavbar } from "./navbar";
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const UseProfileLayout: React.FC<Props> = observer((props) => {
|
||||
const { children } = props;
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const isAuthorized = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
const windowSize = useSize();
|
||||
const isSmallerScreen = windowSize[0] >= 768;
|
||||
|
||||
const { data: userProjectsData } = useSWR(
|
||||
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
|
||||
workspaceSlug && userId
|
||||
? () => userService.getUserProfileProjectsSegregation(workspaceSlug.toString(), userId.toString())
|
||||
: null
|
||||
);
|
||||
// derived values
|
||||
const isAuthorizedPath =
|
||||
pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");
|
||||
const isIssuesTab = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");
|
||||
|
||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||
const currentTab = tabsList.find((tab) => pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Passing the type prop from the current route value as we need the header as top most component.
|
||||
TODO: We are depending on the route path to handle the mobile header type. If the path changes, this logic will break. */}
|
||||
<div className="h-full w-full flex flex-col md:flex-row overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<AppHeader
|
||||
header={
|
||||
<UserProfileHeader
|
||||
type={currentTab?.i18n_label}
|
||||
userProjectsData={userProjectsData}
|
||||
showProfileIssuesFilter={isIssuesTab}
|
||||
/>
|
||||
}
|
||||
mobileHeader={isIssuesTab && <ProfileIssuesMobileHeader />}
|
||||
/>
|
||||
<ContentWrapper>
|
||||
<div className="h-full w-full flex flex-row md:flex-col md:overflow-hidden">
|
||||
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
|
||||
<ProfileNavbar isAuthorized={!!isAuthorized} />
|
||||
{isAuthorized || !isAuthorizedPath ? (
|
||||
<div className={`w-full overflow-hidden h-full`}>{children}</div>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center text-custom-text-200">
|
||||
{t("you_do_not_have_the_permission_to_access_this_page")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isSmallerScreen && <ProfileSidebar userProjectsData={userProjectsData} />}
|
||||
</div>
|
||||
</ContentWrapper>
|
||||
</div>
|
||||
{isSmallerScreen && <ProfileSidebar userProjectsData={userProjectsData} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default UseProfileLayout;
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueLayouts,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
|
||||
export const ProfileIssuesMobileHeader = observer(() => {
|
||||
// plane i18n
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
// store hook
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROFILE);
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
{ layout: layout as EIssueLayoutTypes | undefined },
|
||||
userId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_FILTERS,
|
||||
updatedDisplayFilter,
|
||||
userId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.DISPLAY_PROPERTIES,
|
||||
property,
|
||||
userId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
|
||||
<CustomMenu
|
||||
maxHeight={"md"}
|
||||
className="flex flex-grow justify-center text-sm text-custom-text-200"
|
||||
placement="bottom-start"
|
||||
customButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
{t("common.layout")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200 my-auto" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
customButtonClassName="flex flex-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
{ISSUE_LAYOUTS.map((layout, index) => {
|
||||
if (layout.key === "spreadsheet" || layout.key === "gantt_chart" || layout.key === "calendar") return;
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
handleLayoutChange(ISSUE_LAYOUTS[index].key);
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{t(layout.i18n_title)}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
{t("common.display")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
||||
// components
|
||||
// constants
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isAuthorized: boolean;
|
||||
};
|
||||
|
||||
export const ProfileNavbar: React.FC<Props> = (props) => {
|
||||
const { isAuthorized } = props;
|
||||
const { t } = useTranslation();
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||
|
||||
return (
|
||||
<Header variant={EHeaderVariant.SECONDARY} showOnMobile={false}>
|
||||
<div className="flex items-center overflow-x-scroll">
|
||||
{tabsList.map((tab) => (
|
||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
||||
<span
|
||||
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${
|
||||
pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
>
|
||||
{t(tab.i18n_label)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { GROUP_CHOICES } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { IUserStateDistribution, TStateGroups } from "@plane/types";
|
||||
import { ContentWrapper } from "@plane/ui";
|
||||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { ProfileActivity } from "@/components/profile/overview/activity";
|
||||
import { ProfilePriorityDistribution } from "@/components/profile/overview/priority-distribution";
|
||||
import { ProfileStateDistribution } from "@/components/profile/overview/state-distribution";
|
||||
import { ProfileStats } from "@/components/profile/overview/stats";
|
||||
import { ProfileWorkload } from "@/components/profile/overview/workload";
|
||||
// constants
|
||||
import { USER_PROFILE_DATA } from "@/constants/fetch-keys";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
const userService = new UserService();
|
||||
|
||||
export default function ProfileOverviewPage() {
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { data: userProfile } = useSWR(
|
||||
workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null,
|
||||
workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null
|
||||
);
|
||||
|
||||
const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => {
|
||||
const group = userProfile?.state_distribution.find((g) => g.state_group === key);
|
||||
|
||||
if (group) return group;
|
||||
else return { state_group: key as TStateGroups, state_count: 0 };
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={t("profile.page_label")} />
|
||||
<ContentWrapper className="space-y-7">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2">
|
||||
<ProfilePriorityDistribution userProfile={userProfile} />
|
||||
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
|
||||
</div>
|
||||
<ProfileActivity />
|
||||
</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user