style: kanban board

This commit is contained in:
Aaryan Khandelwal 2023-02-28 14:42:46 +05:30
parent 1b369feb6a
commit 0cd3bb5956
11 changed files with 200 additions and 68 deletions

View File

@ -40,8 +40,13 @@ export const AllBoards: React.FC<Props> = ({
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
<div className="h-full w-full overflow-hidden">
<div className="h-full w-full">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
<div className="flex h-full gap-x-12 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)
: null;
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
@ -56,6 +61,7 @@ export const AllBoards: React.FC<Props> = ({
<SingleBoard
key={index}
type={type}
currentState={currentState}
bgColor={bgColor}
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}

View File

@ -1,22 +1,17 @@
import React from "react";
// react-beautiful-dnd
import { DraggableProvided } from "react-beautiful-dnd";
// icons
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IProjectMember, NestedKeyOf } from "types";
import { IIssue, IProjectMember, IState, NestedKeyOf } from "types";
import { getStateGroupIcon } from "components/icons";
type Props = {
groupedByIssues: {
[key: string]: IIssue[];
};
currentState?: IState | null;
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
bgColor?: string;
@ -28,6 +23,7 @@ type Props = {
export const BoardHeader: React.FC<Props> = ({
groupedByIssues,
currentState,
selectedGroup,
groupTitle,
bgColor,
@ -60,16 +56,13 @@ export const BoardHeader: React.FC<Props> = ({
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
className={`flex cursor-pointer items-center gap-x-3.5 ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
style={{
border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}}
>
{currentState && getStateGroupIcon(currentState.group)}
<h2
className={`text-[0.9rem] font-medium capitalize`}
className={`text-xl font-semibold capitalize`}
style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
@ -80,14 +73,16 @@ export const BoardHeader: React.FC<Props> = ({
? assignees
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="ml-0.5 text-sm text-gray-500">{groupedByIssues[groupTitle].length}</span>
<span className="ml-0.5 text-sm bg-gray-100 py-1 px-3 rounded-full">
{groupedByIssues[groupTitle].length}
</span>
</div>
</div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-200"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
@ -100,7 +95,7 @@ export const BoardHeader: React.FC<Props> = ({
</button>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-200"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />

View File

@ -14,10 +14,11 @@ import { CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
type Props = {
type?: "issue" | "cycle" | "module";
currentState?: IState | null;
bgColor?: string;
groupTitle: string;
groupedByIssues: {
@ -37,6 +38,7 @@ type Props = {
export const SingleBoard: React.FC<Props> = ({
type,
currentState,
bgColor,
groupTitle,
groupedByIssues,
@ -71,10 +73,11 @@ export const SingleBoard: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}>
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
<BoardHeader
addIssueToState={addIssueToState}
currentState={currentState}
bgColor={bgColor}
selectedGroup={selectedGroup}
groupTitle={groupTitle}

View File

@ -184,7 +184,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
return (
<div
className={`rounded border bg-white shadow-sm mb-3 ${
className={`rounded bg-white shadow mb-3 ${
snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : ""
}`}
ref={provided.innerRef}
@ -192,7 +192,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)}
>
<div className="group/card relative select-none p-2">
<div className="group/card relative select-none p-4">
{!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
{type && !isNotAllowed && (
@ -214,19 +214,19 @@ export const SingleBoardIssue: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a>
{properties.key && (
<div className="mb-2 text-xs font-medium text-gray-500">
<div className="mb-2.5 text-xs font-medium text-gray-700">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<h5
className="mb-3 text-sm group-hover:text-theme"
className="text-sm group-hover:text-theme"
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
>
{issue.name}
</h5>
</a>
</Link>
<div className="relative flex flex-wrap items-center gap-x-1 gap-y-2 text-xs">
<div className="relative flex flex-wrap items-center gap-2 mt-2.5 text-xs">
{properties.priority && selectedGroup !== "priority" && (
<ViewPrioritySelect
issue={issue}

View File

@ -0,0 +1,21 @@
import React from "react";
import type { Props } from "./types";
export const BacklogStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "black",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="10" cy="10" r="9" stroke={color} strokeLinecap="round" strokeDasharray="4 4" />
</svg>
);

View File

@ -0,0 +1,33 @@
import React from "react";
import type { Props } from "./types";
export const CompletedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#438af3",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="10"
cy="10"
r="9"
stroke={color}
strokeLinecap="round"
strokeDasharray="0 20 0 10"
/>
<circle cx="10" cy="10" r="7" fill={color} />
<path
d="M13 8.33328L9 12.3333L7.16666 10.4999L7.63666 10.0299L9 11.3899L12.53 7.86328L13 8.33328Z"
fill="white"
/>
</svg>
);

View File

@ -1,4 +1,5 @@
export * from "./attachment-icon";
export * from "./backlog-state-icon";
export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./bolt-icon";
@ -7,6 +8,7 @@ export * from "./cancel-icon";
export * from "./clipboard-icon";
export * from "./comment-icon";
export * from "./completed-cycle-icon";
export * from "./completed-state-icon";
export * from "./current-cycle-icon";
export * from "./cycle-icon";
export * from "./discord-icon";
@ -16,6 +18,7 @@ export * from "./ellipsis-horizontal-icon";
export * from "./external-link-icon";
export * from "./github-icon";
export * from "./heartbeat-icon";
export * from "./started-state-icon";
export * from "./layer-diagonal-icon";
export * from "./lock-icon";
export * from "./menu-icon";
@ -23,6 +26,8 @@ export * from "./plus-icon";
export * from "./question-mark-circle-icon";
export * from "./setting-icon";
export * from "./signal-cellular-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./tag-icon";
export * from "./tune-icon";
export * from "./upcoming-cycle-icon";

View File

@ -0,0 +1,36 @@
import React from "react";
import type { Props } from "./types";
export const StartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#fcbe1d",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="10"
cy="10"
r="9"
stroke={color}
strokeLinecap="round"
strokeDasharray="0 20 0 10"
/>
<path
d="M14.2878 4.46695C13.0513 3.5087 11.5294 2.99227 9.96503 3.00009C8.40068 3.0079 6.88403 3.53951 5.65713 4.51006L10 10L14.2878 4.46695Z"
fill={color}
/>
<path
d="M5.70047 15.5331C6.93701 16.4913 8.45889 17.0077 10.0232 16.9999C11.5876 16.9921 13.1043 16.4605 14.3312 15.4899L9.98828 10L5.70047 15.5331Z"
fill={color}
/>
</svg>
);

View File

@ -0,0 +1,25 @@
import { BacklogStateIcon } from "./backlog-state-icon";
import { CompletedStateIcon } from "./completed-state-icon";
import { StartedStateIcon } from "./started-state-icon";
export const getStateGroupIcon = (
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
width = "20",
height = "20",
color?: string
) => {
switch (stateGroup) {
case "backlog":
return <BacklogStateIcon width={width} height={height} color={color} />;
case "unstarted":
return <StartedStateIcon width={width} height={height} color={color} />;
case "started":
return <StartedStateIcon width={width} height={height} color={color} />;
case "completed":
return <CompletedStateIcon width={width} height={height} color={color} />;
case "cancelled":
return <StartedStateIcon width={width} height={height} color={color} />;
default:
return <></>;
}
};

View File

@ -23,34 +23,38 @@ export const ViewPrioritySelect: React.FC<Props> = ({
isNotAllowed,
}) => (
<CustomSelect
label={
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</span>
</Tooltip>
}
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ priority: data });
}}
maxHeight="md"
buttonClassName={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600 hover:bg-red-100"
: issue.priority === "high"
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
: issue.priority === "low"
? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-gray-100"
} border-none`}
customButton={
<button
type="button"
className={`grid place-items-center rounded w-6 h-6 ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600 hover:bg-red-100"
: issue.priority === "high"
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
: issue.priority === "low"
? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-gray-100"
} border-none`}
>
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</span>
</Tooltip>
</button>
}
noChevron
disabled={isNotAllowed}
selfPositioned={selfPositioned}

View File

@ -8,13 +8,13 @@ type CustomSelectProps = {
value: any;
onChange: any;
children: React.ReactNode;
label: string | JSX.Element;
label?: string | JSX.Element;
textAlignment?: "left" | "center" | "right";
maxHeight?: "sm" | "rg" | "md" | "lg" | "none";
width?: "auto" | string;
input?: boolean;
noChevron?: boolean;
buttonClassName?: string;
customButton?: JSX.Element;
optionsClassName?: string;
disabled?: boolean;
selfPositioned?: boolean;
@ -30,7 +30,7 @@ const CustomSelect = ({
width = "auto",
input = false,
noChevron = false,
buttonClassName = "",
customButton,
optionsClassName = "",
disabled = false,
selfPositioned = false,
@ -43,22 +43,26 @@ const CustomSelect = ({
disabled={disabled}
>
<div>
<Listbox.Button
className={`${buttonClassName} flex w-full ${
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100"
} items-center justify-between gap-1 rounded-md border shadow-sm duration-300 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
input ? "border-gray-300 px-3 py-2 text-sm" : "px-2 py-1 text-xs"
} ${
textAlignment === "right"
? "text-right"
: textAlignment === "center"
? "text-center"
: "text-left"
}`}
>
{label}
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
</Listbox.Button>
{customButton ? (
customButton
) : (
<Listbox.Button
className={`flex w-full ${
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100"
} items-center justify-between gap-1 rounded-md border shadow-sm duration-300 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
input ? "border-gray-300 px-3 py-2 text-sm" : "px-2 py-1 text-xs"
} ${
textAlignment === "right"
? "text-right"
: textAlignment === "center"
? "text-center"
: "text-left"
}`}
>
{label}
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
</Listbox.Button>
)}
</div>
<Transition