mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
style: responsive profile (#3596)
* style: responsive profile * style: profile header drop down, sidebar auto show hide depending on the screen width * fix: item tap on white space in the drop down menu in profile header * fix: profile layout breaking on big screens in page visit
This commit is contained in:
parent
9545dc77d6
commit
55afef204d
@ -1,18 +1,78 @@
|
|||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs, CustomMenu } from "@plane/ui";
|
||||||
import { BreadcrumbLink } from "components/common";
|
import { BreadcrumbLink } from "components/common";
|
||||||
// components
|
// components
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { useApplication, useUser } from "hooks/store";
|
||||||
|
import { ChevronDown, PanelRight } from "lucide-react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export const UserProfileHeader = () => (
|
type TUserProfileHeader = {
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
type?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
||||||
|
const { type = undefined } = props
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, userId } = router.query;
|
||||||
|
|
||||||
|
const AUTHORIZED_ROLES = [20, 15, 10];
|
||||||
|
const {
|
||||||
|
membership: { currentWorkspaceRole },
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
|
if (!currentWorkspaceRole) return null;
|
||||||
|
|
||||||
|
const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
|
||||||
|
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||||
|
|
||||||
|
const { theme: themStore } = useApplication();
|
||||||
|
|
||||||
|
return (<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
<SidebarHamburgerToggle />
|
||||||
<div>
|
<div className="flex justify-between w-full">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} />
|
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
<div className="flex gap-4 md:hidden">
|
||||||
|
<CustomMenu
|
||||||
|
maxHeight={"md"}
|
||||||
|
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={
|
||||||
|
<div className="flex gap-2 items-center px-2 py-1.5 border border-custom-border-400 rounded-md">
|
||||||
|
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{type}</span>
|
||||||
|
<ChevronDown className="w-4 h-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"
|
||||||
|
>
|
||||||
|
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`} className="text-custom-text-300 w-full">{tab.label}</Link>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
<button className="transition-all block md:hidden" onClick={() => { themStore.toggleProfileSidebar(); console.log(themStore.profileSidebarCollapsed) }}>
|
||||||
|
<PanelRight className={
|
||||||
|
cn("w-4 h-4 block md:hidden", !themStore.profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")
|
||||||
|
} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,13 +22,12 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
|
|||||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky -top-0.5 z-10 flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
|
<div className="sticky -top-0.5 z-10 hidden md:flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
|
||||||
<div className="flex items-center overflow-x-scroll">
|
<div className="flex items-center overflow-x-scroll">
|
||||||
{tabsList.map((tab) => (
|
{tabsList.map((tab) => (
|
||||||
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
|
||||||
<span
|
<span
|
||||||
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${
|
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${router.pathname === tab.selected
|
||||||
router.pathname === tab.selected
|
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
: "border-transparent"
|
: "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
|
@ -4,7 +4,7 @@ import useSWR from "swr";
|
|||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "hooks/store";
|
import { useApplication, useUser } from "hooks/store";
|
||||||
// services
|
// services
|
||||||
import { UserService } from "services/user.service";
|
import { UserService } from "services/user.service";
|
||||||
// components
|
// components
|
||||||
@ -18,6 +18,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
@ -28,6 +30,8 @@ export const ProfileSidebar = observer(() => {
|
|||||||
const { workspaceSlug, userId } = router.query;
|
const { workspaceSlug, userId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
|
const { theme: themStore } = useApplication();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { data: userProjectsData } = useSWR(
|
const { data: userProjectsData } = useSWR(
|
||||||
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
|
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
|
||||||
@ -36,6 +40,14 @@ export const ProfileSidebar = observer(() => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useOutsideClickDetector(ref, () => {
|
||||||
|
if (themStore.profileSidebarCollapsed === false) {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
themStore.toggleProfileSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const userDetails = [
|
const userDetails = [
|
||||||
{
|
{
|
||||||
label: "Joined on",
|
label: "Joined on",
|
||||||
@ -47,8 +59,26 @@ export const ProfileSidebar = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleToggleProfileSidebar = () => {
|
||||||
|
if (window && window.innerWidth < 768) {
|
||||||
|
themStore.toggleProfileSidebar(true);
|
||||||
|
}
|
||||||
|
if (window && themStore.profileSidebarCollapsed && window.innerWidth >= 768) {
|
||||||
|
themStore.toggleProfileSidebar(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleToggleProfileSidebar);
|
||||||
|
handleToggleProfileSidebar();
|
||||||
|
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
|
||||||
|
}, [themStore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex-shrink-0 overflow-y-auto shadow-custom-shadow-sm md:h-full md:w-80 border-l border-custom-border-100">
|
<div
|
||||||
|
className={`flex-shrink-0 overflow-hidden overflow-y-auto shadow-custom-shadow-sm border-l border-custom-border-100 bg-custom-sidebar-background-100 h-full z-[5] fixed md:relative transition-all w-full md:w-[300px]`}
|
||||||
|
style={themStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
|
||||||
|
>
|
||||||
{userProjectsData ? (
|
{userProjectsData ? (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-32">
|
<div className="relative h-32">
|
||||||
@ -132,8 +162,7 @@ export const ProfileSidebar = observer(() => {
|
|||||||
{project.assigned_issues > 0 && (
|
{project.assigned_issues > 0 && (
|
||||||
<Tooltip tooltipContent="Completion percentage" position="left">
|
<Tooltip tooltipContent="Completion percentage" position="left">
|
||||||
<div
|
<div
|
||||||
className={`rounded px-1 py-0.5 text-xs font-medium ${
|
className={`rounded px-1 py-0.5 text-xs font-medium ${completedIssuePercentage <= 35
|
||||||
completedIssuePercentage <= 35
|
|
||||||
? "bg-red-500/10 text-red-500"
|
? "bg-red-500/10 text-red-500"
|
||||||
: completedIssuePercentage <= 70
|
: completedIssuePercentage <= 70
|
||||||
? "bg-yellow-500/10 text-yellow-500"
|
? "bg-yellow-500/10 text-yellow-500"
|
||||||
|
@ -2,6 +2,11 @@ import { FC, ReactNode } from "react";
|
|||||||
// layout
|
// layout
|
||||||
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
import { ProfileSettingsLayout } from "layouts/settings-layout";
|
||||||
import { ProfilePreferenceSettingsSidebar } from "./sidebar";
|
import { ProfilePreferenceSettingsSidebar } from "./sidebar";
|
||||||
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface IProfilePreferenceSettingsLayout {
|
interface IProfilePreferenceSettingsLayout {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -10,9 +15,57 @@ interface IProfilePreferenceSettingsLayout {
|
|||||||
|
|
||||||
export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayout> = (props) => {
|
export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayout> = (props) => {
|
||||||
const { children, header } = props;
|
const { children, header } = props;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const showMenuItem = () => {
|
||||||
|
const item = router.asPath.split('/');
|
||||||
|
let splittedItem = item[item.length - 1];
|
||||||
|
splittedItem = splittedItem.replace(splittedItem[0], splittedItem[0].toUpperCase());
|
||||||
|
console.log(splittedItem);
|
||||||
|
return splittedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profilePreferenceLinks: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
label: "Theme",
|
||||||
|
href: `/profile/preferences/theme`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Email",
|
||||||
|
href: `/profile/preferences/email`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProfileSettingsLayout>
|
<ProfileSettingsLayout header={
|
||||||
|
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
|
||||||
|
<SidebarHamburgerToggle />
|
||||||
|
<CustomMenu
|
||||||
|
maxHeight={"md"}
|
||||||
|
className="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={
|
||||||
|
<div className="flex gap-2 items-center px-2 py-1.5 border rounded-md border-custom-border-400">
|
||||||
|
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{showMenuItem()}</span>
|
||||||
|
<ChevronDown className="w-4 h-4 text-custom-text-400" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm"
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
{profilePreferenceLinks.map((link) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Link key={link.href} href={link.href} className="text-custom-text-300 w-full">{link.label}</Link>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
<div className="relative flex h-screen w-full overflow-hidden">
|
<div className="relative flex h-screen w-full overflow-hidden">
|
||||||
<ProfilePreferenceSettingsSidebar />
|
<ProfilePreferenceSettingsSidebar />
|
||||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||||
|
@ -19,15 +19,14 @@ export const ProfilePreferenceSettingsSidebar = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="flex w-96 flex-col gap-6 px-8 py-12">
|
<div className="hidden md:flex w-96 flex-col gap-6 px-8 py-12">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<span className="text-xs font-semibold text-custom-text-400">Preference</span>
|
<span className="text-xs font-semibold text-custom-text-400">Preference</span>
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
{profilePreferenceLinks.map((link) => (
|
{profilePreferenceLinks.map((link) => (
|
||||||
<Link key={link.href} href={link.href}>
|
<Link key={link.href} href={link.href}>
|
||||||
<div
|
<div
|
||||||
className={`rounded-md px-4 py-2 text-sm font-medium ${
|
className={`rounded-md px-4 py-2 text-sm font-medium ${(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
|
||||||
(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
|
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
}`}
|
}`}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
|
|||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { PROFILE_ACTION_LINKS } from "constants/profile";
|
import { PROFILE_ACTION_LINKS } from "constants/profile";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
|
||||||
const WORKSPACE_ACTION_LINKS = [
|
const WORKSPACE_ACTION_LINKS = [
|
||||||
{
|
{
|
||||||
@ -52,6 +53,35 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useOutsideClickDetector(ref, () => {
|
||||||
|
if (sidebarCollapsed === false) {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
toggleSidebar(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
};
|
||||||
|
}, [toggleSidebar]);
|
||||||
|
|
||||||
|
const handleItemClick = () => {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
@ -73,15 +103,17 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative ${
|
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative
|
||||||
sidebarCollapsed ? "" : "md:w-[280px]"
|
${sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||||
} ${sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
|
sm:${sidebarCollapsed ? "-ml-[280px]" : ""}
|
||||||
|
md:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||||
|
lg:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full flex-col gap-y-4">
|
<div ref={ref} className="flex h-full w-full flex-col gap-y-4">
|
||||||
<Link href={`/${redirectWorkspaceSlug}`}>
|
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${
|
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${sidebarCollapsed ? "justify-center" : ""
|
||||||
sidebarCollapsed ? "justify-center" : ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
||||||
@ -101,11 +133,10 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
if (link.key === "change-password" && currentUser?.is_password_autoset) return null;
|
if (link.key === "change-password" && currentUser?.is_password_autoset) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={link.key} href={link.href} className="block w-full">
|
<Link key={link.key} href={link.href} className="block w-full" onClick={handleItemClick}>
|
||||||
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
||||||
<div
|
<div
|
||||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
|
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.pathname)
|
||||||
link.highlight(router.pathname)
|
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
|
||||||
} ${sidebarCollapsed ? "justify-center" : ""}`}
|
} ${sidebarCollapsed ? "justify-center" : ""}`}
|
||||||
@ -129,18 +160,16 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
<Link
|
<Link
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
href={`/${workspace.slug}`}
|
href={`/${workspace.slug}`}
|
||||||
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
|
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${sidebarCollapsed ? "justify-center" : `justify-between`
|
||||||
sidebarCollapsed ? "justify-center" : `justify-between`
|
}`}
|
||||||
|
onClick={handleItemClick}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${
|
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${!workspace?.logo && "rounded bg-custom-primary-500 text-white"
|
||||||
sidebarCollapsed ? "justify-center" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
|
|
||||||
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{workspace?.logo && workspace.logo !== "" ? (
|
{workspace?.logo && workspace.logo !== "" ? (
|
||||||
@ -163,11 +192,10 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
)}
|
)}
|
||||||
<div className="mt-1.5">
|
<div className="mt-1.5">
|
||||||
{WORKSPACE_ACTION_LINKS.map((link) => (
|
{WORKSPACE_ACTION_LINKS.map((link) => (
|
||||||
<Link className="block w-full" key={link.key} href={link.href}>
|
<Link className="block w-full" key={link.key} href={link.href} onClick={handleItemClick}>
|
||||||
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
|
||||||
<div
|
<div
|
||||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${
|
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
|
||||||
sidebarCollapsed ? "justify-center" : ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{<link.Icon className="h-4 w-4" />}
|
{<link.Icon className="h-4 w-4" />}
|
||||||
@ -180,8 +208,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 flex-grow items-end px-6 py-2">
|
<div className="flex flex-shrink-0 flex-grow items-end px-6 py-2">
|
||||||
<div
|
<div
|
||||||
className={`flex w-full ${
|
className={`flex w-full ${sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
|
||||||
sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -202,8 +229,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
|
className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${sidebarCollapsed ? "w-full" : ""
|
||||||
sidebarCollapsed ? "w-full" : ""
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleSidebar()}
|
onClick={() => toggleSidebar()}
|
||||||
>
|
>
|
||||||
|
@ -28,9 +28,8 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
|
|||||||
const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed");
|
const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden">
|
<div className="h-full w-full realtive flex flex-row">
|
||||||
<ProfileSidebar />
|
<div className="w-full realtive flex flex-col">
|
||||||
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
|
|
||||||
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
|
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
|
||||||
{isAuthorized || !isAuthorizedPath ? (
|
{isAuthorized || !isAuthorizedPath ? (
|
||||||
<div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div>
|
<div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div>
|
||||||
@ -40,6 +39,8 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ProfileSidebar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,7 @@ const ProfileAssignedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage t
|
|||||||
|
|
||||||
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader />}>
|
<AppLayout header={<UserProfileHeader type="Assigned" />}>
|
||||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -14,7 +14,7 @@ const ProfileCreatedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage ty
|
|||||||
|
|
||||||
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader />}>
|
<AppLayout header={<UserProfileHeader type="Created" />}>
|
||||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -56,7 +56,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
|
|||||||
|
|
||||||
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader />}>
|
<AppLayout header={<UserProfileHeader type='Summary' />}>
|
||||||
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -14,7 +14,7 @@ const ProfileSubscribedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage
|
|||||||
|
|
||||||
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<AppLayout header={<UserProfileHeader />}>
|
<AppLayout header={<UserProfileHeader type="Subscribed" />}>
|
||||||
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
@ -21,6 +21,7 @@ import { USER_ACTIVITY } from "constants/fetch-keys";
|
|||||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||||
// type
|
// type
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
@ -30,8 +31,10 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
|||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mx-auto mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
|
|
||||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
<section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
|
||||||
|
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
|
||||||
|
<SidebarHamburgerToggle />
|
||||||
<h3 className="text-xl font-medium">Activity</h3>
|
<h3 className="text-xl font-medium">Activity</h3>
|
||||||
</div>
|
</div>
|
||||||
{userActivity ? (
|
{userActivity ? (
|
||||||
@ -187,6 +190,7 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
|
|||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import { ProfileSettingsLayout } from "layouts/settings-layout";
|
|||||||
import { Button, Input, Spinner } from "@plane/ui";
|
import { Button, Input, Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
old_password: string;
|
old_password: string;
|
||||||
@ -86,6 +87,10 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
|
||||||
|
<SidebarHamburgerToggle />
|
||||||
|
</div>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(handleChangePassword)}
|
onSubmit={handleSubmit(handleChangePassword)}
|
||||||
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
|
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
|
||||||
@ -168,6 +173,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import type { NextPageWithLayout } from "lib/types";
|
|||||||
// constants
|
// constants
|
||||||
import { USER_ROLES } from "constants/workspace";
|
import { USER_ROLES } from "constants/workspace";
|
||||||
import { TIME_ZONES } from "constants/timezones";
|
import { TIME_ZONES } from "constants/timezones";
|
||||||
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
|
|
||||||
const defaultValues: Partial<IUser> = {
|
const defaultValues: Partial<IUser> = {
|
||||||
avatar: "",
|
avatar: "",
|
||||||
@ -136,6 +137,11 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
|
||||||
|
<SidebarHamburgerToggle />
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="avatar"
|
name="avatar"
|
||||||
@ -155,7 +161,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
|
||||||
<div className="mx-auto flex h-full w-full flex-col space-y-10 overflow-y-auto pt-16 px-8 pb-8 lg:w-3/5">
|
<div className="mx-auto flex h-full w-full flex-col space-y-10 overflow-y-auto pt-10 md:pt-16 px-8 pb-8 lg:w-3/5">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="flex w-full flex-col gap-8">
|
<div className="flex w-full flex-col gap-8">
|
||||||
<div className="relative h-44 w-full">
|
<div className="relative h-44 w-full">
|
||||||
@ -289,8 +295,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.email)}
|
hasError={Boolean(errors.email)}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${
|
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${errors.email ? "border-red-500" : ""
|
||||||
errors.email ? "border-red-500" : ""
|
|
||||||
}`}
|
}`}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
@ -435,6 +440,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
|||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -48,7 +48,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentUser ? (
|
{currentUser ? (
|
||||||
<div className="mx-auto mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
|
<div className="mx-auto mt-10 md:mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
|
||||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||||
<h3 className="text-xl font-medium">Preferences</h3>
|
<h3 className="text-xl font-medium">Preferences</h3>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,15 +7,18 @@ export interface IThemeStore {
|
|||||||
// observables
|
// observables
|
||||||
theme: string | null;
|
theme: string | null;
|
||||||
sidebarCollapsed: boolean | undefined;
|
sidebarCollapsed: boolean | undefined;
|
||||||
|
profileSidebarCollapsed: boolean | undefined;
|
||||||
// actions
|
// actions
|
||||||
toggleSidebar: (collapsed?: boolean) => void;
|
toggleSidebar: (collapsed?: boolean) => void;
|
||||||
setTheme: (theme: any) => void;
|
setTheme: (theme: any) => void;
|
||||||
|
toggleProfileSidebar: (collapsed?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThemeStore implements IThemeStore {
|
export class ThemeStore implements IThemeStore {
|
||||||
// observables
|
// observables
|
||||||
sidebarCollapsed: boolean | undefined = undefined;
|
sidebarCollapsed: boolean | undefined = undefined;
|
||||||
theme: string | null = null;
|
theme: string | null = null;
|
||||||
|
profileSidebarCollapsed: boolean | undefined = undefined;
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
|
|
||||||
@ -24,9 +27,11 @@ export class ThemeStore implements IThemeStore {
|
|||||||
// observable
|
// observable
|
||||||
sidebarCollapsed: observable.ref,
|
sidebarCollapsed: observable.ref,
|
||||||
theme: observable.ref,
|
theme: observable.ref,
|
||||||
|
profileSidebarCollapsed: observable.ref,
|
||||||
// action
|
// action
|
||||||
toggleSidebar: action,
|
toggleSidebar: action,
|
||||||
setTheme: action,
|
setTheme: action,
|
||||||
|
toggleProfileSidebar: action,
|
||||||
// computed
|
// computed
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
@ -46,6 +51,19 @@ export class ThemeStore implements IThemeStore {
|
|||||||
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the profile sidebar collapsed state
|
||||||
|
* @param collapsed
|
||||||
|
*/
|
||||||
|
toggleProfileSidebar = (collapsed?: boolean) => {
|
||||||
|
if (collapsed === undefined) {
|
||||||
|
this.profileSidebarCollapsed = !this.profileSidebarCollapsed;
|
||||||
|
} else {
|
||||||
|
this.profileSidebarCollapsed = collapsed;
|
||||||
|
}
|
||||||
|
localStorage.setItem("profile_sidebar_collapsed", this.profileSidebarCollapsed.toString());
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the user theme and applies it to the platform
|
* Sets the user theme and applies it to the platform
|
||||||
* @param _theme
|
* @param _theme
|
||||||
|
Loading…
Reference in New Issue
Block a user