forked from github/plane
style: profile page responsiveness added (#1710)
* refactor: folder structure * style: mobile responsiveness added * chore: add user profile redirection * chore: profile page authorization
This commit is contained in:
parent
406b323e8e
commit
0586d30a33
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -143,13 +144,17 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
{activityItem.field === "archived_at" &&
|
||||
activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : (
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name}
|
||||
{activityItem.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activityItem.actor_detail.last_name}
|
||||
{activityItem.actor_detail.first_name} Bot
|
||||
</span>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
||||
<a className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name}{" "}
|
||||
{activityItem.actor_detail.last_name}
|
||||
</a>
|
||||
</Link>
|
||||
)}{" "}
|
||||
{message}{" "}
|
||||
<span className="whitespace-nowrap">
|
||||
|
@ -1,15 +1,26 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
// components
|
||||
import { ProfileIssuesViewOptions } from "components/profile";
|
||||
// types
|
||||
import { UserAuth } from "types";
|
||||
|
||||
const tabsList = [
|
||||
type Props = {
|
||||
memberRole: UserAuth;
|
||||
};
|
||||
|
||||
const viewerTabs = [
|
||||
{
|
||||
route: "",
|
||||
label: "Overview",
|
||||
selected: "/[workspaceSlug]/profile/[userId]",
|
||||
},
|
||||
];
|
||||
|
||||
const adminTabs = [
|
||||
{
|
||||
route: "assigned",
|
||||
label: "Assigned",
|
||||
@ -27,12 +38,17 @@ const tabsList = [
|
||||
},
|
||||
];
|
||||
|
||||
export const ProfileNavbar = () => {
|
||||
export const ProfileNavbar: React.FC<Props> = ({ memberRole }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const tabsList =
|
||||
memberRole.isOwner || memberRole.isMember || memberRole.isViewer
|
||||
? [...viewerTabs, ...adminTabs]
|
||||
: viewerTabs;
|
||||
|
||||
return (
|
||||
<div className="px-4 sm:px-5 flex items-center justify-between gap-4 border-b border-custom-border-300">
|
||||
<div className="sticky -top-0.5 z-[1] md:static px-4 sm:px-5 flex items-center justify-between gap-4 bg-custom-background-100 border-b border-custom-border-300">
|
||||
<div className="flex items-center overflow-x-scroll">
|
||||
{tabsList.map((tab) => (
|
||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
||||
|
91
apps/app/components/profile/overview/activity.tsx
Normal file
91
apps/app/components/profile/overview/activity.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
// helpers
|
||||
import { activityDetails } from "helpers/activity.helper";
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// fetch-keys
|
||||
import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
export const ProfileActivity = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const { data: userProfileActivity } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString())
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () => userService.getUserProfileActivity(workspaceSlug.toString(), userId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Recent Activity</h3>
|
||||
<div className="border border-custom-border-100 rounded p-6">
|
||||
{userProfileActivity ? (
|
||||
<div className="space-y-5">
|
||||
{userProfileActivity.results.map((activity) => (
|
||||
<div key={activity.id} className="flex gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-1 w-4/5 break-words">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
activityDetails[activity.field]?.message(activity as any)
|
||||
) : (
|
||||
<span>
|
||||
created this{" "}
|
||||
<a
|
||||
href={`/${activity.workspace_detail.slug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
Issue
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200">{timeAgo(activity.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export * from "./activity";
|
||||
export * from "./priority-distribution";
|
||||
export * from "./state-distribution";
|
||||
export * from "./stats";
|
||||
|
@ -10,7 +10,7 @@ type Props = {
|
||||
export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Workload</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-4 justify-stretch">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 justify-stretch">
|
||||
{stateDistribution.map((group) => (
|
||||
<div key={group.state_group}>
|
||||
<a className="flex gap-2 p-4 rounded border border-custom-border-100 whitespace-nowrap">
|
||||
@ -21,7 +21,13 @@ export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => (
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-1 -mt-1">
|
||||
<p className="text-custom-text-400 text-sm capitalize">{group.state_group}</p>
|
||||
<p className="text-custom-text-400 text-sm capitalize">
|
||||
{group.state_group === "unstarted"
|
||||
? "Not Started"
|
||||
: group.state_group === "started"
|
||||
? "Working on"
|
||||
: group.state_group}
|
||||
</p>
|
||||
<p className="text-xl font-semibold">{group.state_count}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -10,7 +10,7 @@ import useEstimateOption from "hooks/use-estimate-option";
|
||||
// components
|
||||
import { MyIssuesSelectFilters } from "components/issues";
|
||||
// ui
|
||||
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
||||
import { CustomMenu, CustomSearchSelect, ToggleSwitch, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
|
||||
@ -21,6 +21,7 @@ import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
import { Properties, TIssueViewOptions } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
import useProjects from "hooks/use-projects";
|
||||
|
||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
{
|
||||
@ -37,6 +38,8 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
const {
|
||||
issueView,
|
||||
setIssueView,
|
||||
@ -54,12 +57,28 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
const options = projects?.map((project) => ({
|
||||
value: project.id,
|
||||
query: project.name + " " + project.identifier,
|
||||
content: project.name,
|
||||
}));
|
||||
|
||||
if (
|
||||
!router.pathname.includes("assigned") &&
|
||||
!router.pathname.includes("created") &&
|
||||
!router.pathname.includes("subscribed")
|
||||
)
|
||||
return null;
|
||||
// return (
|
||||
// <CustomSearchSelect
|
||||
// value={projects ?? null}
|
||||
// onChange={(val: string[] | null) => console.log(val)}
|
||||
// label="Filters"
|
||||
// options={options}
|
||||
// position="right"
|
||||
// multiple
|
||||
// />
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -8,10 +9,14 @@ import { useTheme } from "next-themes";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
import { Icon, Loader, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { EditOutlined } from "@mui/icons-material";
|
||||
// helpers
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
import { render12HourFormatTime, renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// fetch-keys
|
||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
||||
@ -22,6 +27,8 @@ export const ProfileSidebar = () => {
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: userProjectsData } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
|
||||
@ -33,27 +40,24 @@ export const ProfileSidebar = () => {
|
||||
);
|
||||
|
||||
const userDetails = [
|
||||
{
|
||||
label: "Username",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
label: "Joined on",
|
||||
value: renderLongDetailDateFormat(userProjectsData?.user_data.date_joined ?? ""),
|
||||
},
|
||||
{
|
||||
label: "Timezone",
|
||||
value: userProjectsData?.user_data.user_timezone,
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
value: "Online",
|
||||
value: (
|
||||
<span>
|
||||
{render12HourFormatTime(new Date())}{" "}
|
||||
<span className="text-custom-text-200">{userProjectsData?.user_data.user_timezone}</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 h-full w-80 overflow-y-auto"
|
||||
className="flex-shrink-0 md:h-full w-full md:w-80 overflow-y-auto"
|
||||
style={{
|
||||
boxShadow:
|
||||
theme === "light"
|
||||
@ -64,10 +68,23 @@ export const ProfileSidebar = () => {
|
||||
{userProjectsData ? (
|
||||
<>
|
||||
<div className="relative h-32">
|
||||
{user?.id === userId && (
|
||||
<div className="absolute top-3.5 right-3.5 h-5 w-5 bg-white rounded grid place-items-center">
|
||||
<Link href={`/${workspaceSlug}/me/profile`}>
|
||||
<a className="grid place-items-center text-black">
|
||||
<EditOutlined
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={
|
||||
userProjectsData.user_data.cover_image ??
|
||||
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
|
||||
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
|
||||
}
|
||||
alt={userProjectsData.user_data.first_name}
|
||||
className="h-32 w-full object-cover"
|
||||
@ -96,8 +113,8 @@ export const ProfileSidebar = () => {
|
||||
<div className="mt-6 space-y-5">
|
||||
{userDetails.map((detail) => (
|
||||
<div key={detail.label} className="flex items-center gap-4 text-sm">
|
||||
<div className="text-custom-text-200 w-2/5">{detail.label}</div>
|
||||
<div className="font-medium">{detail.value}</div>
|
||||
<div className="flex-shrink-0 text-custom-text-200 w-2/5">{detail.label}</div>
|
||||
<div className="font-medium w-3/5 break-words">{detail.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -143,17 +160,19 @@ export const ProfileSidebar = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<div
|
||||
className={`px-1 py-0.5 text-xs font-medium rounded ${
|
||||
completedIssuePercentage <= 35
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: completedIssuePercentage <= 70
|
||||
? "bg-yellow-500/10 text-yellow-500"
|
||||
: "bg-green-500/10 text-green-500"
|
||||
}`}
|
||||
>
|
||||
{completedIssuePercentage}%
|
||||
</div>
|
||||
<Tooltip tooltipContent="Completion percentage" position="left">
|
||||
<div
|
||||
className={`px-1 py-0.5 text-xs font-medium rounded ${
|
||||
completedIssuePercentage <= 35
|
||||
? "bg-red-500/10 text-red-500"
|
||||
: completedIssuePercentage <= 70
|
||||
? "bg-yellow-500/10 text-yellow-500"
|
||||
: "bg-green-500/10 text-green-500"
|
||||
}`}
|
||||
>
|
||||
{completedIssuePercentage}%
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Icon iconName="arrow_drop_down" className="!text-lg" />
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
|
@ -22,7 +22,7 @@ export type CustomSearchSelectProps = DropdownProps & {
|
||||
| { multiple?: false; value: any } // if multiple is false, value can be anything
|
||||
| {
|
||||
multiple?: true;
|
||||
value: any[]; // if multiple is true, value should be an array
|
||||
value: any[] | null; // if multiple is true, value should be an array
|
||||
}
|
||||
);
|
||||
|
||||
@ -68,7 +68,7 @@ export const CustomSearchSelect = ({
|
||||
className={`${selfPositioned ? "" : "relative"} flex-shrink-0 text-left ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{({ open }: any) => {
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open && onOpen) onOpen();
|
||||
|
||||
return (
|
||||
|
@ -73,8 +73,6 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
|
||||
useEffect(() => {
|
||||
if (!userId || !filters) return;
|
||||
|
||||
console.log("Triggered");
|
||||
|
||||
if (
|
||||
router.pathname.includes("assigned") &&
|
||||
(!filters.assignees || !filters.assignees.includes(userId))
|
||||
|
@ -11,13 +11,17 @@ import { IProject } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECTS_LIST } from "constants/fetch-keys";
|
||||
|
||||
const useProjects = (type?: "all" | boolean) => {
|
||||
const useProjects = (type?: "all" | boolean, fetchCondition?: boolean) => {
|
||||
fetchCondition = fetchCondition ?? true;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: projects, mutate: mutateProjects } = useSWR(
|
||||
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string, { is_favorite: type ?? "all" }) : null,
|
||||
workspaceSlug
|
||||
workspaceSlug && fetchCondition
|
||||
? PROJECTS_LIST(workspaceSlug as string, { is_favorite: type ?? "all" })
|
||||
: null,
|
||||
workspaceSlug && fetchCondition
|
||||
? () => projectService.getProjects(workspaceSlug as string, { is_favorite: type ?? "all" })
|
||||
: null
|
||||
);
|
||||
|
53
apps/app/layouts/profile-layout.tsx
Normal file
53
apps/app/layouts/profile-layout.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// components
|
||||
import { ProfileNavbar, ProfileSidebar } from "components/profile";
|
||||
// ui
|
||||
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ProfileAuthWrapper = (props: Props) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||
<BreadcrumbItem title={`User Name`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<ProfileLayout {...props} />
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileLayout: React.FC<Props> = ({ children, className }) => {
|
||||
const { memberRole } = useWorkspaceMyMembership();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden">
|
||||
<ProfileSidebar />
|
||||
<div className="md:h-full w-full flex flex-col md:overflow-hidden">
|
||||
<ProfileNavbar memberRole={memberRole} />
|
||||
{memberRole.isOwner || memberRole.isMember || memberRole.isViewer ? (
|
||||
<div className={`md:h-full w-full overflow-hidden ${className}`}>{children}</div>
|
||||
) : (
|
||||
<div className="h-full w-full grid place-items-center text-custom-text-200">
|
||||
You do not have the permission to access this page.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,48 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// contexts
|
||||
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
||||
// components
|
||||
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { ProfileIssuesView } from "components/profile";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const ProfileAssignedIssues: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ProfileIssuesContextProvider>
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
|
||||
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileIssuesView />
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
};
|
||||
const ProfileAssignedIssues: NextPage = () => (
|
||||
<ProfileIssuesContextProvider>
|
||||
<ProfileAuthWrapper>
|
||||
<ProfileIssuesView />
|
||||
</ProfileAuthWrapper>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
|
||||
export default ProfileAssignedIssues;
|
||||
|
@ -1,48 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// contexts
|
||||
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
||||
// components
|
||||
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { ProfileIssuesView } from "components/profile";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const ProfileCreatedIssues: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ProfileIssuesContextProvider>
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
|
||||
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileIssuesView />
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
};
|
||||
const ProfileCreatedIssues: NextPage = () => (
|
||||
<ProfileIssuesContextProvider>
|
||||
<ProfileAuthWrapper>
|
||||
<ProfileIssuesView />
|
||||
</ProfileAuthWrapper>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
|
||||
export default ProfileCreatedIssues;
|
||||
|
@ -1,34 +1,26 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// layouts
|
||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
||||
// components
|
||||
import {
|
||||
ProfileNavbar,
|
||||
ProfileActivity,
|
||||
ProfilePriorityDistribution,
|
||||
ProfileSidebar,
|
||||
ProfileStateDistribution,
|
||||
ProfileStats,
|
||||
ProfileWorkload,
|
||||
} from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { Icon, Loader } from "components/ui";
|
||||
// helpers
|
||||
import { activityDetails } from "helpers/activity.helper";
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import { IUserStateDistribution, TStateGroups } from "types";
|
||||
// constants
|
||||
import { USER_PROFILE_DATA, USER_PROFILE_ACTIVITY } from "constants/fetch-keys";
|
||||
import { USER_PROFILE_DATA } from "constants/fetch-keys";
|
||||
import { GROUP_CHOICES } from "constants/project";
|
||||
|
||||
const ProfileOverview: NextPage = () => {
|
||||
@ -42,15 +34,6 @@ const ProfileOverview: NextPage = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: userProfileActivity } = useSWR(
|
||||
workspaceSlug && userId
|
||||
? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString())
|
||||
: null,
|
||||
workspaceSlug && userId
|
||||
? () => userService.getUserProfileActivity(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);
|
||||
|
||||
@ -59,93 +42,20 @@ const ProfileOverview: NextPage = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||
<BreadcrumbItem title={`User Name`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full overflow-y-auto px-9 py-5 space-y-7">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
|
||||
<ProfilePriorityDistribution userProfile={userProfile} />
|
||||
<ProfileStateDistribution
|
||||
stateDistribution={stateDistribution}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Recent Activity</h3>
|
||||
<div className="border border-custom-border-100 rounded p-6">
|
||||
{userProfileActivity ? (
|
||||
<div className="space-y-5">
|
||||
{userProfileActivity.results.map((activity) => (
|
||||
<div key={activity.id} className="flex gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-6 w-6 place-items-center rounded border-2 bg-gray-700 text-xs text-white">
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="-mt-1">
|
||||
<p className="text-sm text-custom-text-200">
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
|
||||
</span>
|
||||
{activity.field ? (
|
||||
activityDetails[activity.field]?.message(activity as any)
|
||||
) : (
|
||||
<span>
|
||||
created this{" "}
|
||||
<Link
|
||||
href={`/${activity.workspace_detail.slug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||
>
|
||||
<a className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline">
|
||||
Issue
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-custom-text-200">
|
||||
{timeAgo(activity.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="space-y-5">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProfileAuthWrapper>
|
||||
<div className="h-full w-full px-5 md:px-9 py-5 space-y-7 overflow-y-auto">
|
||||
<ProfileStats userProfile={userProfile} />
|
||||
<ProfileWorkload stateDistribution={stateDistribution} />
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
|
||||
<ProfilePriorityDistribution userProfile={userProfile} />
|
||||
<ProfileStateDistribution
|
||||
stateDistribution={stateDistribution}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
<ProfileActivity />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</ProfileAuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,48 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// contexts
|
||||
import { ProfileIssuesContextProvider } from "contexts/profile-issues-context";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import { ProfileAuthWrapper } from "layouts/profile-layout";
|
||||
// components
|
||||
import { ProfileIssuesView, ProfileNavbar, ProfileSidebar } from "components/profile";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { ProfileIssuesView } from "components/profile";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const ProfileSubscribedIssues: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<ProfileIssuesContextProvider>
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Settings" link={`/${workspaceSlug}/me/profile`} />
|
||||
<BreadcrumbItem title={`${user?.first_name} ${user?.last_name}`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileNavbar />
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<ProfileIssuesView />
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSidebar />
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
};
|
||||
const ProfileSubscribedIssues: NextPage = () => (
|
||||
<ProfileIssuesContextProvider>
|
||||
<ProfileAuthWrapper>
|
||||
<ProfileIssuesView />
|
||||
</ProfileAuthWrapper>
|
||||
</ProfileIssuesContextProvider>
|
||||
);
|
||||
|
||||
export default ProfileSubscribedIssues;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
@ -187,9 +188,17 @@ const MembersSettings: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm">
|
||||
{member.first_name} {member.last_name}
|
||||
</h4>
|
||||
{member.member ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="text-sm">
|
||||
{member.first_name} {member.last_name}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h4 className="text-sm">
|
||||
{member.first_name} {member.last_name}
|
||||
</h4>
|
||||
)}
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,7 +14,6 @@ import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
import SettingsNavbar from "layouts/settings-navbar";
|
||||
// components
|
||||
import { ImageUploadModal } from "components/core";
|
||||
import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -187,9 +187,17 @@ const MembersSettings: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm">
|
||||
{member.first_name} {member.last_name}
|
||||
</h4>
|
||||
{member.member ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
|
||||
<a className="text-sm">
|
||||
{member.first_name} {member.last_name}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<h4 className="text-sm">
|
||||
{member.first_name} {member.last_name}
|
||||
</h4>
|
||||
)}
|
||||
<p className="text-xs text-custom-text-200">{member.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user