style: new cycle list (#374)

* feat: short date helper function

* feat: linear progress indicator added

* style: new cyce list and cycle card design

* feat: short date function improve

* feat: linear progress indicator improvement

* style: cycle card and progress indicator

* fix: helper date function and progress indicator fix

* fix: build error

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2023-03-06 11:36:48 +05:30 committed by GitHub
parent fef72ccc70
commit 786816ed41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 285 additions and 180 deletions

View File

@ -1,15 +1,18 @@
// react
import { useState } from "react"; import { useState } from "react";
// next
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// services
import cyclesService from "services/cycles.service";
// components // components
import { DeleteCycleModal, SingleCycleCard } from "components/cycles"; import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
// icons
import { CompletedCycleIcon } from "components/icons";
// types // types
import { ICycle, SelectCycleType } from "types"; import { ICycle, SelectCycleType } from "types";
import { CompletedCycleIcon } from "components/icons"; // fetch-keys
import cyclesService from "services/cycles.service";
import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys"; import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys";
export interface CompletedCyclesListProps { export interface CompletedCyclesListProps {
@ -58,14 +61,16 @@ export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
data={selectedCycleForDelete} data={selectedCycleForDelete}
/> />
{completedCycles?.completed_cycles.length > 0 ? ( {completedCycles?.completed_cycles.length > 0 ? (
completedCycles.completed_cycles.map((cycle) => ( <div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<SingleCycleCard {completedCycles.completed_cycles.map((cycle) => (
key={cycle.id} <SingleCycleCard
cycle={cycle} key={cycle.id}
handleDeleteCycle={() => handleDeleteCycle(cycle)} cycle={cycle}
handleEditCycle={() => handleEditCycle(cycle)} handleDeleteCycle={() => handleDeleteCycle(cycle)}
/> handleEditCycle={() => handleEditCycle(cycle)}
)) />
))}
</div>
) : ( ) : (
<div className="flex flex-col items-center justify-center gap-4 text-center"> <div className="flex flex-col items-center justify-center gap-4 text-center">
<CompletedCycleIcon height="56" width="56" /> <CompletedCycleIcon height="56" width="56" />

View File

@ -1,10 +1,11 @@
// react
import { useState } from "react"; import { useState } from "react";
// components // components
import { DeleteCycleModal, SingleCycleCard } from "components/cycles"; import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
// icons
import { CompletedCycleIcon, CurrentCycleIcon, UpcomingCycleIcon } from "components/icons";
// types // types
import { ICycle, SelectCycleType } from "types"; import { ICycle, SelectCycleType } from "types";
import { CompletedCycleIcon, CurrentCycleIcon, UpcomingCycleIcon } from "components/icons";
type TCycleStatsViewProps = { type TCycleStatsViewProps = {
cycles: ICycle[]; cycles: ICycle[];
@ -44,14 +45,16 @@ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
data={selectedCycleForDelete} data={selectedCycleForDelete}
/> />
{cycles.length > 0 ? ( {cycles.length > 0 ? (
cycles.map((cycle) => ( <div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<SingleCycleCard {cycles.map((cycle) => (
key={cycle.id} <SingleCycleCard
cycle={cycle} key={cycle.id}
handleDeleteCycle={() => handleDeleteCycle(cycle)} cycle={cycle}
handleEditCycle={() => handleEditCycle(cycle)} handleDeleteCycle={() => handleDeleteCycle(cycle)}
/> handleEditCycle={() => handleEditCycle(cycle)}
)) />
))}
</div>
) : ( ) : (
<div className="flex flex-col items-center justify-center gap-4 text-center"> <div className="flex flex-col items-center justify-center gap-4 text-center">
{type === "upcoming" ? ( {type === "upcoming" ? (

View File

@ -4,22 +4,22 @@ import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// swr
import useSWR from "swr"; import useSWR from "swr";
// services // services
import cyclesService from "services/cycles.service"; import cyclesService from "services/cycles.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { Button, CustomMenu } from "components/ui"; import { CustomMenu, LinearProgressIndicator } from "components/ui";
import { Disclosure, Transition } from "@headlessui/react";
// icons // icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid"; import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import { UserIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon, PencilIcon, StarIcon } from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
// helpers // helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper"; import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { groupBy } from "helpers/array.helper"; import { groupBy } from "helpers/array.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
// types // types
import { CycleIssueResponse, ICycle } from "types"; import { CycleIssueResponse, ICycle } from "types";
// fetch-keys // fetch-keys
@ -34,11 +34,11 @@ type TSingleStatProps = {
const stateGroupColours: { const stateGroupColours: {
[key: string]: string; [key: string]: string;
} = { } = {
backlog: "#3f76ff", backlog: "#DEE2E6",
unstarted: "#ff9e9e", unstarted: "#26B5CE",
started: "#d687ff", started: "#F7AE59",
cancelled: "#ff5353", cancelled: "#D687FF",
completed: "#096e8d", completed: "#09A953",
}; };
export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => { export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
@ -82,100 +82,138 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
}); });
}; };
const progressIndicatorData = Object.keys(groupedIssues).map((group, index) => ({
id: index,
name: capitalizeFirstLetter(group),
value:
cycleIssues && cycleIssues.length > 0
? (groupedIssues[group].length / cycleIssues.length) * 100
: 0,
color: stateGroupColours[group],
}));
return ( return (
<> <div className="h-full w-full">
<div className="rounded-md border bg-white p-3"> <div className="flex flex-col rounded-[10px] bg-white text-xs shadow">
<div className="grid grid-cols-9 gap-2 divide-x"> <Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<div className="col-span-3 flex flex-col space-y-3"> <a>
<div className="flex items-start justify-between gap-2"> <div className="flex h-full flex-col gap-4 rounded-b-[10px] px-5 py-5">
<Link href={`/${workspaceSlug}/projects/${projectId as string}/cycles/${cycle.id}`}> <div className="flex items-center justify-between gap-1">
<a> <h3 className="text-xl font-semibold leading-5">{cycle.name}</h3>
<h2 className="font-medium w-full max-w-[175px] lg:max-w-[225px] xl:max-w-[300px] text-ellipsis overflow-hidden"> {/* <span className="p-1">
{cycle.name} <StarIcon className="h-4 w-4 " color="#858E96" />
</h2> </span> */}
</a> </div>
</Link>
<CustomMenu width="auto" ellipsis> <div className="flex items-center justify-start gap-5">
<CustomMenu.MenuItem onClick={handleEditCycle}>Edit cycle</CustomMenu.MenuItem> <div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4 text-gray-900" />
<span className="text-gray-400">Start :</span>
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4 text-gray-900" />
<span className="text-gray-400">End :</span>
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</div>
</div>
</a>
</Link>
<div className="flex h-full flex-col rounded-b-[10px]">
<div className="flex items-center justify-between px-5 py-4">
<div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
/>
) : (
<button
className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white"
onClick={handleEditCycle}
>
{cycle.owned_by.first_name.charAt(0)}
</button>
)}
<span className="text-gray-900">{cycle.owned_by.first_name}</span>
</div>
<div className="flex items-center ">
<button
onClick={handleEditCycle}
className="flex cursor-pointer items-center rounded p-1 duration-300 hover:bg-gray-100"
>
<span>
<PencilIcon className="h-4 w-4" />
</span>
</button>
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem onClick={handleDeleteCycle}>Delete cycle</CustomMenu.MenuItem> <CustomMenu.MenuItem onClick={handleDeleteCycle}>Delete cycle</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>Copy cycle link</CustomMenu.MenuItem> <CustomMenu.MenuItem onClick={handleCopyText}>Copy cycle link</CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </div>
<div className="grid grid-cols-3 gap-x-2 gap-y-3 text-xs"> </div>
<div className="flex items-center gap-2 text-gray-500">
<CalendarDaysIcon className="h-4 w-4" /> <Disclosure>
Cycle dates {({ open }) => (
</div> <div
<div className="col-span-2"> className={`flex h-full w-full flex-col border-t border-gray-200 bg-gray-100 ${
{renderShortNumericDateFormat(startDate)} - {renderShortNumericDateFormat(endDate)} open ? "" : "flex-row"
</div> }`}
<div className="flex items-center gap-2 text-gray-500">
<UserIcon className="h-4 w-4" />
Created by
</div>
<div className="col-span-2 flex items-center gap-2">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
/>
) : (
<span className="grid h-5 w-5 place-items-center rounded-full bg-gray-700 capitalize text-white">
{cycle.owned_by.first_name.charAt(0)}
</span>
)}
{cycle.owned_by.first_name}
</div>
</div>
<div className="flex h-full items-end">
<Button
theme="secondary"
className="flex items-center gap-2"
onClick={() =>
router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)
}
> >
<CyclesIcon className="h-3 w-3" /> <div className="flex w-full items-center gap-2 px-5 py-4 ">
Open Cycle <span> Progress </span>
</Button> <LinearProgressIndicator data={progressIndicatorData} />
</div> <Disclosure.Button>
</div> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
<div className="col-span-2 space-y-3 px-5"> </Disclosure.Button>
<h4 className="text-sm tracking-widest">PROGRESS</h4>
<div className="space-y-3 text-xs">
{Object.keys(groupedIssues).map((group) => (
<div key={group} className="flex items-center gap-2">
<div className="flex basis-2/3 items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: stateGroupColours[group],
}}
/>
<h6 className="text-xs capitalize">{group}</h6>
</div>
<div>
<span>
{groupedIssues[group].length}{" "}
<span className="text-gray-500">
-{" "}
{cycleIssues && cycleIssues.length > 0
? `${Math.round(
(groupedIssues[group].length / cycleIssues.length) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</div> </div>
))} <Transition show={open}>
</div> <Disclosure.Panel>
</div> <div className="overflow-hidden rounded-b-md bg-white p-3 shadow">
<div className="col-span-2 space-y-3 px-5">
<div className="space-y-3 text-xs">
{Object.keys(groupedIssues).map((group) => (
<div key={group} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: stateGroupColours[group],
}}
/>
<h6 className="text-xs capitalize">{group}</h6>
</div>
<div>
<span>
{groupedIssues[group].length}{" "}
<span className="text-gray-500">
-{" "}
{cycleIssues && cycleIssues.length > 0
? `${Math.round(
(groupedIssues[group].length / cycleIssues.length) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</div>
))}
</div>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div> </div>
</div> </div>
</> </div>
); );
}; };

View File

@ -16,3 +16,4 @@ export * from "./progress-bar";
export * from "./spinner"; export * from "./spinner";
export * from "./tooltip"; export * from "./tooltip";
export * from "./labels-list"; export * from "./labels-list";
export * from "./linear-progress-indicator";

View File

@ -0,0 +1,32 @@
import React from "react";
import { Tooltip } from "./tooltip";
type Props = {
data: any;
};
export const LinearProgressIndicator: React.FC<Props> = ({ data }) => {
const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0);
let progress = 0;
const bars = data.map((item: any) => {
const width = `${(item.value / total) * 100}%`;
const style = {
width,
backgroundColor: item.color,
};
progress += item.value;
return (
<Tooltip tooltipContent={`${item.name} ${item.value}%`}>
<div key={item.id} className="bar" style={style} />
</Tooltip>
);
});
return (
<div className="flex h-1 w-full items-center justify-between gap-1">
{total === 0 ? " - 0%" : <div className="flex h-full w-full gap-1 rounded-md">{bars}</div>}
</div>
);
};

View File

@ -101,4 +101,12 @@ export const getDateRangeStatus = (startDate: string , endDate: string ) => {
} else { } else {
return "upcoming"; return "upcoming";
} }
}
export const renderShortDateWithYearFormat = (date: Date) => {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
return isNaN(date.getTime()) ? "N/A" : ` ${month} ${day}, ${year}`;
} }

View File

@ -2,10 +2,11 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import useSWR from "swr"; import useSWR from "swr";
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// lib // lib
import { requiredAuth } from "lib/auth"; import { requiredAuth } from "lib/auth";
@ -110,68 +111,85 @@ const ProjectCycles: NextPage = () => {
data={selectedCycle} data={selectedCycle}
/> />
<div className="space-y-8"> <div className="space-y-8">
<h3 className="text-xl font-medium leading-6 text-gray-900">Current Cycle</h3> <div className="flex flex-col gap-5">
<div className="space-y-5"> <h3 className="text-3xl font-semibold text-black">Current Cycle</h3>
<CyclesList <div className="space-y-5">
cycles={currentAndUpcomingCycles?.current_cycle ?? []} <CyclesList
setCreateUpdateCycleModal={setCreateUpdateCycleModal} cycles={currentAndUpcomingCycles?.current_cycle ?? []}
setSelectedCycle={setSelectedCycle} setCreateUpdateCycleModal={setCreateUpdateCycleModal}
type="current" setSelectedCycle={setSelectedCycle}
/> type="current"
/>
</div>
</div> </div>
<div className="space-y-5"> <div className="flex flex-col gap-5">
<Tab.Group> <h3 className="text-3xl font-semibold text-black">Others</h3>
<Tab.List <div>
as="div" <Tab.Group>
className="flex justify-between items-center gap-2 rounded-lg bg-gray-100 p-2 text-sm" <Tab.List
> as="div"
<Tab className="flex items-center justify-start gap-4 text-base font-medium"
className={({ selected }) =>
`w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
}
> >
Upcoming <Tab
</Tab> className={({ selected }) =>
<Tab ` rounded-3xl border px-6 py-3 ${
className={({ selected }) => selected
`w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` ? "bg-theme text-white"
} : "border-gray-400 bg-white text-gray-900 hover:bg-gray-200"
> }`
Completed }
</Tab> >
<Tab Upcoming
className={({ selected }) => </Tab>
` w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` <Tab
} className={({ selected }) =>
> ` rounded-3xl border px-6 py-3 ${
Draft selected
</Tab> ? "bg-theme text-white"
</Tab.List> : "border-gray-400 bg-white text-gray-900 hover:bg-gray-200"
<Tab.Panels> }`
<Tab.Panel as="div" className="mt-8 space-y-5"> }
<CyclesList >
cycles={currentAndUpcomingCycles?.upcoming_cycle ?? []} Completed
setCreateUpdateCycleModal={setCreateUpdateCycleModal} </Tab>
setSelectedCycle={setSelectedCycle} <Tab
type="upcoming" className={({ selected }) =>
/> ` rounded-3xl border px-6 py-3 ${
</Tab.Panel> selected
<Tab.Panel as="div" className="mt-8 space-y-5"> ? "bg-theme text-white"
<CompletedCyclesList : "border-gray-400 bg-white text-gray-900 hover:bg-gray-200"
setCreateUpdateCycleModal={setCreateUpdateCycleModal} }`
setSelectedCycle={setSelectedCycle} }
/> >
</Tab.Panel> Drafts
</Tab.Panels> </Tab>
<Tab.Panel as="div" className="mt-8 space-y-5"> </Tab.List>
<CyclesList <Tab.Panels>
cycles={draftCycles?.draft_cycles ?? []} <Tab.Panel as="div" className="mt-8 space-y-5">
setCreateUpdateCycleModal={setCreateUpdateCycleModal} <CyclesList
setSelectedCycle={setSelectedCycle} cycles={currentAndUpcomingCycles?.upcoming_cycle ?? []}
type="upcoming" setCreateUpdateCycleModal={setCreateUpdateCycleModal}
/> setSelectedCycle={setSelectedCycle}
</Tab.Panel> type="upcoming"
</Tab.Group> />
</Tab.Panel>
<Tab.Panel as="div" className="mt-8 space-y-5">
<CompletedCyclesList
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
/>
</Tab.Panel>
<Tab.Panel as="div" className="mt-8 space-y-5">
<CyclesList
cycles={draftCycles?.draft_cycles ?? []}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
type="upcoming"
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div> </div>
</div> </div>
</AppLayout> </AppLayout>