mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
style/projects_page
This commit is contained in:
parent
1b369feb6a
commit
76b8b9eaef
@ -1,122 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
// ui
|
|
||||||
// icons
|
|
||||||
import {
|
|
||||||
CalendarDaysIcon,
|
|
||||||
CheckIcon,
|
|
||||||
PencilIcon,
|
|
||||||
PlusIcon,
|
|
||||||
TrashIcon,
|
|
||||||
ClipboardDocumentListIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// types
|
|
||||||
// ui
|
|
||||||
import { Button } from "components/ui";
|
|
||||||
// hooks
|
|
||||||
import useProjectMembers from "hooks/use-project-members";
|
|
||||||
// helpers
|
|
||||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
|
||||||
// types
|
|
||||||
import type { IProject } from "types";
|
|
||||||
|
|
||||||
export type ProjectCardProps = {
|
|
||||||
workspaceSlug: string;
|
|
||||||
project: IProject;
|
|
||||||
setToJoinProject: (id: string | null) => void;
|
|
||||||
setDeleteProject: (id: string | null) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectCard: React.FC<ProjectCardProps> = (props) => {
|
|
||||||
const { workspaceSlug, project, setToJoinProject, setDeleteProject } = props;
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
// fetching project members information
|
|
||||||
const { members, isMember, canDelete, canEdit } = useProjectMembers(workspaceSlug, project.id);
|
|
||||||
|
|
||||||
if (!members) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-36 w-full flex-col rounded-md bg-white px-4 py-3">
|
|
||||||
<div className="h-full w-full animate-pulse bg-gray-50" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex h-full w-full flex-col rounded-md border bg-white px-4 py-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex gap-2 text-lg font-medium">
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${project.id}/issues`}>
|
|
||||||
<a className="flex items-center gap-x-3">
|
|
||||||
{project.icon && (
|
|
||||||
<span className="text-base">{String.fromCodePoint(parseInt(project.icon))}</span>
|
|
||||||
)}
|
|
||||||
<span className=" w-auto max-w-[220px] text-ellipsis whitespace-nowrap overflow-hidden">
|
|
||||||
{project.name}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{isMember ? (
|
|
||||||
<div className="flex">
|
|
||||||
{canEdit && (
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${project.id}/settings`}>
|
|
||||||
<a className="grid h-7 w-7 cursor-pointer place-items-center rounded p-1 duration-300 hover:bg-gray-100">
|
|
||||||
<PencilIcon className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{canDelete && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100"
|
|
||||||
onClick={() => setDeleteProject(project.id)}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm">{project.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 flex h-full items-end justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
theme="secondary"
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
onClick={() => router.push(`/${workspaceSlug}/projects/${project.id}/issues`)}
|
|
||||||
>
|
|
||||||
<ClipboardDocumentListIcon className="h-3 w-3" />
|
|
||||||
Open Project
|
|
||||||
</Button>
|
|
||||||
{!isMember ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setToJoinProject(project.id);
|
|
||||||
}}
|
|
||||||
className="flex cursor-pointer items-center gap-1 rounded border p-2 text-xs font-medium duration-300 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-3 w-3" />
|
|
||||||
<span>Select to Join</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1 text-xs">
|
|
||||||
<CheckIcon className="h-3 w-3" />
|
|
||||||
Member
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mb-1 flex items-center gap-1 text-xs">
|
|
||||||
<CalendarDaysIcon className="h-4 w-4" />
|
|
||||||
{renderShortNumericDateFormat(project.created_at)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
export * from "./card";
|
export * from "./single-project-card";
|
||||||
export * from "./create-project-modal";
|
export * from "./create-project-modal";
|
||||||
export * from "./join-project";
|
export * from "./join-project";
|
||||||
export * from "./sidebar-list";
|
export * from "./sidebar-list";
|
||||||
|
131
apps/app/components/project/single-project-card.tsx
Normal file
131
apps/app/components/project/single-project-card.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu, Loader } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { CalendarDaysIcon, PencilIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { StarIcon } from "@heroicons/react/20/solid";
|
||||||
|
// helpers
|
||||||
|
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import type { IProject } from "types";
|
||||||
|
|
||||||
|
export type ProjectCardProps = {
|
||||||
|
project: IProject;
|
||||||
|
setToJoinProject: (id: string | null) => void;
|
||||||
|
setDeleteProject: (id: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SingleProjectCard: React.FC<ProjectCardProps> = ({
|
||||||
|
project,
|
||||||
|
setToJoinProject,
|
||||||
|
setDeleteProject,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
// fetching project members information
|
||||||
|
const { members, isMember, canDelete, canEdit } = useProjectMembers(
|
||||||
|
workspaceSlug as string,
|
||||||
|
project.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCopyText = () => {
|
||||||
|
const originURL =
|
||||||
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
|
||||||
|
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${project.id}/issues`).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Project link copied to clipboard.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{members ? (
|
||||||
|
<Link href={`/${workspaceSlug as string}/projects/${project.id}/issues`}>
|
||||||
|
<a className="shadow rounded-[10px]">
|
||||||
|
<div
|
||||||
|
className="relative h-32 bg-center bg-cover bg-no-repeat rounded-t-[10px]"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${
|
||||||
|
project.cover_image ??
|
||||||
|
"https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=874&q=80"
|
||||||
|
})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute left-7 bottom-4 flex items-center gap-3 text-white">
|
||||||
|
{!isMember ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setToJoinProject(project.id);
|
||||||
|
}}
|
||||||
|
className="flex cursor-pointer items-center gap-1 rounded border p-2 text-xs font-medium duration-300 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-3 w-3" />
|
||||||
|
<span>Select to Join</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="bg-[#09A953] px-2 py-1 rounded text-xs">Member</span>
|
||||||
|
)}
|
||||||
|
<span className="bg-[#f7ae59] h-6 w-9 grid place-items-center rounded">
|
||||||
|
<StarIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-7 py-4 rounded-b-[10px]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-1.5xl font-semibold">{project.name}</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3.5 mb-7">{project.description}</p>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
|
{renderShortNumericDateFormat(project.created_at)}
|
||||||
|
</div>
|
||||||
|
{isMember ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{canEdit && (
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${project.id}/settings`}>
|
||||||
|
<a className="grid cursor-pointer place-items-center rounded p-1 duration-300 hover:bg-gray-100">
|
||||||
|
<PencilIcon className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
|
<CustomMenu width="auto" verticalEllipsis>
|
||||||
|
<CustomMenu.MenuItem onClick={() => setDeleteProject(project.id)}>
|
||||||
|
Delete project
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
|
Copy project link
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Loader>
|
||||||
|
<Loader.Item height="144px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -11,6 +11,7 @@ type Props = {
|
|||||||
label?: string | JSX.Element;
|
label?: string | JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
ellipsis?: boolean;
|
ellipsis?: boolean;
|
||||||
|
verticalEllipsis?: boolean;
|
||||||
width?: "sm" | "md" | "lg" | "xl" | "auto";
|
width?: "sm" | "md" | "lg" | "xl" | "auto";
|
||||||
textAlignment?: "left" | "center" | "right";
|
textAlignment?: "left" | "center" | "right";
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
@ -30,6 +31,7 @@ const CustomMenu = ({
|
|||||||
label,
|
label,
|
||||||
className = "",
|
className = "",
|
||||||
ellipsis = false,
|
ellipsis = false,
|
||||||
|
verticalEllipsis = false,
|
||||||
width = "auto",
|
width = "auto",
|
||||||
textAlignment,
|
textAlignment,
|
||||||
noBorder = false,
|
noBorder = false,
|
||||||
@ -37,9 +39,9 @@ const CustomMenu = ({
|
|||||||
}: Props) => (
|
}: Props) => (
|
||||||
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
|
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
|
||||||
<div>
|
<div>
|
||||||
{ellipsis ? (
|
{ellipsis || verticalEllipsis ? (
|
||||||
<Menu.Button className="relative grid place-items-center rounded p-1 hover:bg-gray-100 focus:outline-none">
|
<Menu.Button className="relative grid place-items-center rounded p-1 hover:bg-gray-100 focus:outline-none">
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
<EllipsisHorizontalIcon className={`h-4 w-4 ${verticalEllipsis ? "rotate-90" : ""}`} />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
|
@ -10,7 +10,7 @@ import useWorkspaces from "hooks/use-workspaces";
|
|||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { JoinProjectModal } from "components/project/join-project-modal";
|
import { JoinProjectModal } from "components/project/join-project-modal";
|
||||||
import { ProjectCard } from "components/project";
|
import { SingleProjectCard } from "components/project";
|
||||||
import ConfirmProjectDeletion from "components/project/confirm-project-deletion";
|
import ConfirmProjectDeletion from "components/project/confirm-project-deletion";
|
||||||
// ui
|
// ui
|
||||||
import { HeaderButton, EmptySpace, EmptySpaceItem, Loader } from "components/ui";
|
import { HeaderButton, EmptySpace, EmptySpaceItem, Loader } from "components/ui";
|
||||||
@ -101,18 +101,15 @@ const ProjectsPage: NextPage = () => {
|
|||||||
</EmptySpace>
|
</EmptySpace>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full space-y-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-9">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
{projects.map((project) => (
|
||||||
{projects.map((item) => (
|
<SingleProjectCard
|
||||||
<ProjectCard
|
key={project.id}
|
||||||
key={item.id}
|
project={project}
|
||||||
project={item}
|
setToJoinProject={setSelectedProjectToJoin}
|
||||||
workspaceSlug={(activeWorkspace as any)?.slug}
|
setDeleteProject={setDeleteProject}
|
||||||
setToJoinProject={setSelectedProjectToJoin}
|
/>
|
||||||
setDeleteProject={setDeleteProject}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -4,6 +4,13 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.text-1\.5xl {
|
||||||
|
font-size: 1.375rem;
|
||||||
|
line-height: 1.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
@ -24,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-enable::-webkit-scrollbar {
|
.scrollbar-enable::-webkit-scrollbar {
|
||||||
display: block ;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar style */
|
/* Scrollbar style */
|
||||||
|
1
apps/app/types/projects.d.ts
vendored
1
apps/app/types/projects.d.ts
vendored
@ -1,6 +1,7 @@
|
|||||||
import type { IUserLite, IWorkspace } from "./";
|
import type { IUserLite, IWorkspace } from "./";
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
|
cover_image: string | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
cycle_view: boolean;
|
cycle_view: boolean;
|
||||||
|
Loading…
Reference in New Issue
Block a user