fix: merge conflicts

This commit is contained in:
Aaryan Khandelwal 2023-03-03 13:55:18 +05:30
commit e281feddf5
61 changed files with 1955 additions and 786 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-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
<div className="h-full w-full"> <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-9 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => { {Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)
: null;
const stateId = const stateId =
selectedGroup === "state_detail.name" selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null ? states?.find((s) => s.name === singleGroup)?.id ?? null
@ -56,6 +61,7 @@ export const AllBoards: React.FC<Props> = ({
<SingleBoard <SingleBoard
key={index} key={index}
type={type} type={type}
currentState={currentState}
bgColor={bgColor} bgColor={bgColor}
groupTitle={singleGroup} groupTitle={singleGroup}
groupedByIssues={groupedByIssues} groupedByIssues={groupedByIssues}

View File

@ -1,22 +1,17 @@
import React from "react"; import React from "react";
// react-beautiful-dnd
import { DraggableProvided } from "react-beautiful-dnd";
// icons // icons
import { import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
import { IIssue, IProjectMember, NestedKeyOf } from "types"; import { IIssue, IProjectMember, IState, NestedKeyOf } from "types";
import { getStateGroupIcon } from "components/icons";
type Props = { type Props = {
groupedByIssues: { groupedByIssues: {
[key: string]: IIssue[]; [key: string]: IIssue[];
}; };
currentState?: IState | null;
selectedGroup: NestedKeyOf<IIssue> | null; selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string; groupTitle: string;
bgColor?: string; bgColor?: string;
@ -28,6 +23,7 @@ type Props = {
export const BoardHeader: React.FC<Props> = ({ export const BoardHeader: React.FC<Props> = ({
groupedByIssues, groupedByIssues,
currentState,
selectedGroup, selectedGroup,
groupTitle, groupTitle,
bgColor, bgColor,
@ -54,22 +50,19 @@ export const BoardHeader: React.FC<Props> = ({
return ( return (
<div <div
className={`flex justify-between p-3 pb-0 ${ className={`flex justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : "" !isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
}`} }`}
> >
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div <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" : "" !isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`} }`}
style={{
border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}}
> >
{currentState && getStateGroupIcon(currentState.group, "20", "20", bgColor)}
<h2 <h2
className={`text-[0.9rem] font-medium capitalize`} className={`text-xl font-semibold capitalize`}
style={{ style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb", writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}} }}
@ -80,14 +73,16 @@ export const BoardHeader: React.FC<Props> = ({
? assignees ? assignees
: addSpaceIfCamelCase(groupTitle)} : addSpaceIfCamelCase(groupTitle)}
</h2> </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> </div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}> <div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button <button
type="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-100"
onClick={() => { onClick={() => {
setIsCollapsed((prevData) => !prevData); setIsCollapsed((prevData) => !prevData);
}} }}
@ -100,7 +95,7 @@ export const BoardHeader: React.FC<Props> = ({
</button> </button>
<button <button
type="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-100"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />

View File

@ -13,11 +13,14 @@ import { BoardHeader, SingleBoardIssue } from "components/core";
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types // types
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types"; import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
type Props = { type Props = {
type?: "issue" | "cycle" | "module"; type?: "issue" | "cycle" | "module";
currentState?: IState | null;
bgColor?: string; bgColor?: string;
groupTitle: string; groupTitle: string;
groupedByIssues: { groupedByIssues: {
@ -37,6 +40,7 @@ type Props = {
export const SingleBoard: React.FC<Props> = ({ export const SingleBoard: React.FC<Props> = ({
type, type,
currentState,
bgColor, bgColor,
groupTitle, groupTitle,
groupedByIssues, groupedByIssues,
@ -71,10 +75,11 @@ export const SingleBoard: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return ( 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"}`}> <div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
<BoardHeader <BoardHeader
addIssueToState={addIssueToState} addIssueToState={addIssueToState}
currentState={currentState}
bgColor={bgColor} bgColor={bgColor}
selectedGroup={selectedGroup} selectedGroup={selectedGroup}
groupTitle={groupTitle} groupTitle={groupTitle}
@ -86,7 +91,7 @@ export const SingleBoard: React.FC<Props> = ({
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}> <StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className={`relative mt-3 h-full px-3 pb-3 overflow-y-auto ${ className={`relative h-full p-1 overflow-y-auto ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : "" snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!isCollapsed ? "hidden" : "block"}`} } ${!isCollapsed ? "hidden" : "block"}`}
ref={provided.innerRef} ref={provided.innerRef}
@ -104,7 +109,7 @@ export const SingleBoard: React.FC<Props> = ({
snapshot.isDraggingOver ? "block" : "hidden" snapshot.isDraggingOver ? "block" : "hidden"
} top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 text-xs whitespace-nowrap bg-white p-2 rounded pointer-events-none z-[99999999]`} } top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 text-xs whitespace-nowrap bg-white p-2 rounded pointer-events-none z-[99999999]`}
> >
This board is ordered by {orderBy} This board is ordered by {replaceUnderscoreIfSnakeCase(orderBy ?? "")}
</div> </div>
</> </>
)} )}
@ -148,21 +153,23 @@ export const SingleBoard: React.FC<Props> = ({
{type === "issue" ? ( {type === "issue" ? (
<button <button
type="button" type="button"
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100" className="flex items-center gap-2 text-theme font-medium outline-none"
onClick={addIssueToState} onClick={addIssueToState}
> >
<PlusIcon className="mr-1 h-3 w-3" /> <PlusIcon className="h-4 w-4" />
Create Add Issue
</button> </button>
) : ( ) : (
<CustomMenu <CustomMenu
label={ customButton={
<span className="flex items-center gap-1"> <button
<PlusIcon className="h-3 w-3" /> type="button"
Add issue className="flex items-center gap-2 text-theme font-medium outline-none"
</span> >
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
} }
className="mt-1"
optionsPosition="left" optionsPosition="left"
noBorder noBorder
> >

View File

@ -184,15 +184,15 @@ export const SingleBoardIssue: React.FC<Props> = ({
return ( return (
<div <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" : "" snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
}`} }`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)} 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 && ( {!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100"> <div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
{type && !isNotAllowed && ( {type && !isNotAllowed && (
@ -214,19 +214,19 @@ export const SingleBoardIssue: React.FC<Props> = ({
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a> <a>
{properties.key && ( {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} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
)} )}
<h5 <h5
className="mb-3 text-sm group-hover:text-theme" className="text-sm group-hover:text-theme"
style={{ lineClamp: 3, WebkitLineClamp: 3 }} style={{ lineClamp: 3, WebkitLineClamp: 3 }}
> >
{issue.name} {issue.name}
</h5> </h5>
</a> </a>
</Link> </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" && ( {properties.priority && selectedGroup !== "priority" && (
<ViewPrioritySelect <ViewPrioritySelect
issue={issue} issue={issue}

View File

@ -0,0 +1,152 @@
import React, { useEffect, useState, useRef } from "react";
// next
import Image from "next/image";
// swr
import useSWR from "swr";
// headless ui
import { Tab, Transition, Popover } from "@headlessui/react";
// services
import fileService from "services/file.service";
// components
import { Input, Spinner } from "components/ui";
import { PrimaryButton } from "components/ui/button/primary-button";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const tabOptions = [
{
key: "unsplash",
title: "Unsplash",
},
{
key: "upload",
title: "Upload",
},
];
type Props = {
label: string | React.ReactNode;
value: string | null;
onChange: (data: string) => void;
};
export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange }) => {
const ref = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [searchParams, setSearchParams] = useState("");
const [formData, setFormData] = useState({
search: "",
});
const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchParams}`, () =>
fileService.getUnsplashImages(1, searchParams)
);
useOutsideClickDetector(ref, () => {
setIsOpen(false);
});
useEffect(() => {
if (!images || value !== null) return;
onChange(images[0].urls.regular);
}, [value, onChange, images]);
return (
<Popover className="relative z-[2]" ref={ref}>
<Popover.Button
className="rounded-md border border-gray-500 bg-white px-2 py-1 text-xs text-gray-700"
onClick={() => setIsOpen((prev) => !prev)}
>
{label}
</Popover.Button>
<Transition
show={isOpen}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-white shadow-lg">
<div className="h-96 w-80 overflow-auto rounded border bg-white p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
<Tab.Group>
<Tab.List as="span" className="inline-block rounded bg-gray-200 p-1">
{tabOptions.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
selected ? "bg-theme text-white" : "text-black"
}`
}
>
{tab.title}
</Tab>
))}
</Tab.List>
<Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
<Tab.Panel className="h-full w-full space-y-4">
<form
onSubmit={(e) => {
e.preventDefault();
setSearchParams(formData.search);
}}
className="flex gap-x-2 pt-7"
>
<Input
name="search"
className="text-sm"
id="search"
value={formData.search}
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
placeholder="Search for images"
/>
<PrimaryButton className="bg-indigo-600" size="sm">
Search
</PrimaryButton>
</form>
{images ? (
<div className="grid grid-cols-4 gap-4">
{images.map((image) => (
<div
key={image.id}
className="relative col-span-2 aspect-video md:col-span-1"
>
<Image
src={image.urls.small}
alt={image.alt_description}
layout="fill"
objectFit="cover"
className="cursor-pointer rounded"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
}}
/>
</div>
))}
</div>
) : (
<div className="flex justify-center pt-20">
<Spinner />
</div>
)}
</Tab.Panel>
<Tab.Panel className="flex h-full w-full flex-col items-center justify-center">
<p>Coming Soon...</p>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Popover.Panel>
</Transition>
</Popover>
);
};

View File

@ -9,3 +9,4 @@ export * from "./issues-view";
export * from "./link-modal"; export * from "./link-modal";
export * from "./not-authorized-view"; export * from "./not-authorized-view";
export * from "./multi-level-select"; export * from "./multi-level-select";
export * from "./image-picker-popover";

View File

@ -371,13 +371,13 @@ export const IssuesView: React.FC<Props> = ({
<div <div
className={`${ className={`${
trashBox ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none" trashBox ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
} fixed z-20 top-12 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-red-100 border-2 border-red-500 p-3 text-xs rounded ${ } fixed z-20 top-9 right-9 flex justify-center items-center gap-2 bg-red-100 border-2 border-red-500 p-3 w-96 h-28 text-xs italic text-red-500 font-medium rounded ${
snapshot.isDraggingOver ? "bg-red-500 text-white" : "" snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
} duration-200`} } duration-200`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
> >
<TrashIcon className="h-3 w-3" /> <TrashIcon className="h-4 w-4" />
Drop issue here to delete Drop issue here to delete
</div> </div>
)} )}

View File

@ -0,0 +1,82 @@
// react
import { useState } from "react";
// next
import { useRouter } from "next/router";
import useSWR from "swr";
// components
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
// types
import { ICycle, SelectCycleType } from "types";
import { CompletedCycleIcon } from "components/icons";
import cyclesService from "services/cycles.service";
import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys";
export interface CompletedCyclesListProps {
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
}
export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
setCreateUpdateCycleModal,
setSelectedCycle,
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: completedCycles } = useSWR(
workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string)
: null
);
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
setCycleDeleteModal(true);
};
const handleEditCycle = (cycle: ICycle) => {
setSelectedCycle({ ...cycle, actionType: "edit" });
setCreateUpdateCycleModal(true);
};
return (
<>
{completedCycles && (
<>
<DeleteCycleModal
isOpen={
cycleDeleteModal &&
!!selectedCycleForDelete &&
selectedCycleForDelete.actionType === "delete"
}
setIsOpen={setCycleDeleteModal}
data={selectedCycleForDelete}
/>
{completedCycles?.completed_cycles.length > 0 ? (
completedCycles.completed_cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
/>
))
) : (
<div className="flex flex-col items-center justify-center gap-4 text-center">
<CompletedCycleIcon height="56" width="56" />
<h3 className="text-gray-500">
No completed cycles yet. Create with{" "}
<pre className="inline rounded bg-gray-200 px-2 py-1">Q</pre>.
</h3>
</div>
)}
</>
)}
</>
);
};

View File

@ -13,7 +13,7 @@ type TCycleStatsViewProps = {
type: "current" | "upcoming" | "completed"; type: "current" | "upcoming" | "completed";
}; };
export const CyclesListView: React.FC<TCycleStatsViewProps> = ({ export const CyclesList: React.FC<TCycleStatsViewProps> = ({
cycles, cycles,
setCreateUpdateCycleModal, setCreateUpdateCycleModal,
setSelectedCycle, setSelectedCycle,

View File

@ -1,11 +1,16 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/router";
// toast
import useToast from "hooks/use-toast";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// ui // ui
import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui"; import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui";
// types // types
import { ICycle } from "types"; import { ICycle } from "types";
// services
import cyclesService from "services/cycles.service";
type Props = { type Props = {
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>; handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
@ -17,17 +22,24 @@ type Props = {
const defaultValues: Partial<ICycle> = { const defaultValues: Partial<ICycle> = {
name: "", name: "",
description: "", description: "",
status: "draft", start_date: null,
start_date: "", end_date: null,
end_date: "",
}; };
export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => { export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const [isDateValid, setIsDateValid] = useState(true);
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
handleSubmit, handleSubmit,
control, control,
watch,
reset, reset,
} = useForm<ICycle>({ } = useForm<ICycle>({
defaultValues, defaultValues,
@ -41,6 +53,31 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
}); });
}; };
const dateChecker = async (payload: any) => {
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
})
.catch((err) => {
console.log(err);
});
};
const checkEmptyDate =
(watch("start_date") === "" && watch("end_date") === "") ||
(!watch("start_date") && !watch("end_date"));
useEffect(() => { useEffect(() => {
reset({ reset({
...defaultValues, ...defaultValues,
@ -84,30 +121,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
register={register} register={register}
/> />
</div> </div>
<div>
<h6 className="text-gray-500">Status</h6>
<Controller
name="status"
control={control}
render={({ field }) => (
<CustomSelect
{...field}
label={<span className="capitalize">{field.value ?? "Select Status"}</span>}
input
>
{[
{ label: "Draft", value: "draft" },
{ label: "Started", value: "started" },
{ label: "Completed", value: "completed" },
].map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<div className="w-full"> <div className="w-full">
<h6 className="text-gray-500">Start Date</h6> <h6 className="text-gray-500">Start Date</h6>
@ -115,12 +129,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<Controller <Controller
control={control} control={control}
name="start_date" name="start_date"
rules={{ required: "Start date is required" }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomDatePicker <CustomDatePicker
renderAs="input" renderAs="input"
value={value} value={value}
onChange={onChange} onChange={(val) => {
onChange(val);
watch("end_date")
? dateChecker({
start_date: val,
end_date: watch("end_date"),
})
: "";
}}
error={errors.start_date ? true : false} error={errors.start_date ? true : false}
/> />
)} )}
@ -136,12 +157,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<Controller <Controller
control={control} control={control}
name="end_date" name="end_date"
rules={{ required: "End date is required" }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomDatePicker <CustomDatePicker
renderAs="input" renderAs="input"
value={value} value={value}
onChange={onChange} onChange={(val) => {
onChange(val);
watch("start_date")
? dateChecker({
start_date: watch("start_date"),
end_date: val,
})
: "";
}}
error={errors.end_date ? true : false} error={errors.end_date ? true : false}
/> />
)} )}
@ -158,7 +186,18 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<Button theme="secondary" onClick={handleClose}> <Button theme="secondary" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={isSubmitting}>
<Button
type="submit"
className={
checkEmptyDate
? "cursor-pointer"
: isDateValid
? "cursor-pointer"
: "cursor-not-allowed"
}
disabled={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true}
>
{status {status
? isSubmitting ? isSubmitting
? "Updating Cycle..." ? "Updating Cycle..."

View File

@ -1,4 +1,5 @@
export * from "./cycles-list-view"; export * from "./completed-cycles-list";
export * from "./cycles-list";
export * from "./delete-cycle-modal"; export * from "./delete-cycle-modal";
export * from "./form"; export * from "./form";
export * from "./modal"; export * from "./modal";

View File

@ -12,10 +12,16 @@ import cycleService from "services/cycles.service";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { CycleForm } from "components/cycles"; import { CycleForm } from "components/cycles";
// helper
import { getDateRangeStatus } from "helpers/date-time.helper";
// types // types
import type { ICycle } from "types"; import type { ICycle } from "types";
// fetch keys // fetch keys
import { CYCLE_LIST } from "constants/fetch-keys"; import {
CYCLE_COMPLETE_LIST,
CYCLE_CURRENT_AND_UPCOMING_LIST,
CYCLE_DRAFT_LIST,
} from "constants/fetch-keys";
type CycleModalProps = { type CycleModalProps = {
isOpen: boolean; isOpen: boolean;
@ -37,7 +43,23 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
await cycleService await cycleService
.createCycle(workspaceSlug as string, projectId as string, payload) .createCycle(workspaceSlug as string, projectId as string, payload)
.then((res) => { .then((res) => {
mutate(CYCLE_LIST(projectId as string)); switch (
res?.start_date && res.end_date
? getDateRangeStatus(res?.start_date, res.end_date)
: "draft"
) {
case "completed":
mutate(CYCLE_COMPLETE_LIST(projectId as string));
break;
case "current":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
case "upcoming":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
default:
mutate(CYCLE_DRAFT_LIST(projectId as string));
}
handleClose(); handleClose();
setToastAlert({ setToastAlert({
@ -59,7 +81,23 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
await cycleService await cycleService
.updateCycle(workspaceSlug as string, projectId as string, cycleId, payload) .updateCycle(workspaceSlug as string, projectId as string, cycleId, payload)
.then((res) => { .then((res) => {
mutate(CYCLE_LIST(projectId as string)); switch (
res?.start_date && res.end_date
? getDateRangeStatus(res?.start_date, res.end_date)
: "draft"
) {
case "completed":
mutate(CYCLE_COMPLETE_LIST(projectId as string));
break;
case "current":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
case "upcoming":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
default:
mutate(CYCLE_DRAFT_LIST(projectId as string));
}
handleClose(); handleClose();
setToastAlert({ setToastAlert({
@ -113,7 +151,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> <Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<CycleForm <CycleForm
handleFormSubmit={handleFormSubmit} handleFormSubmit={handleFormSubmit}
handleClose={handleClose} handleClose={handleClose}

View File

@ -6,7 +6,7 @@ import Image from "next/image";
import { mutate } from "swr"; import { mutate } from "swr";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
// icons // icons
@ -19,7 +19,7 @@ import {
UserIcon, UserIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// ui // ui
import { CustomSelect, Loader, ProgressBar } from "components/ui"; import { Loader, ProgressBar } from "components/ui";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// services // services
@ -36,17 +36,22 @@ import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-tim
import { CycleIssueResponse, ICycle, IIssue } from "types"; import { CycleIssueResponse, ICycle, IIssue } from "types";
// fetch-keys // fetch-keys
import { CYCLE_DETAILS } from "constants/fetch-keys"; import { CYCLE_DETAILS } from "constants/fetch-keys";
// constants
import { CYCLE_STATUS } from "constants/cycle";
type Props = { type Props = {
issues: IIssue[]; issues: IIssue[];
cycle: ICycle | undefined; cycle: ICycle | undefined;
isOpen: boolean; isOpen: boolean;
cycleIssues: CycleIssueResponse[]; cycleIssues: CycleIssueResponse[];
cycleStatus: string;
}; };
export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cycleIssues }) => { export const CycleDetailsSidebar: React.FC<Props> = ({
issues,
cycle,
isOpen,
cycleIssues,
cycleStatus,
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const router = useRouter(); const router = useRouter();
@ -60,7 +65,6 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
const defaultValues: Partial<ICycle> = { const defaultValues: Partial<ICycle> = {
start_date: new Date().toString(), start_date: new Date().toString(),
end_date: new Date().toString(), end_date: new Date().toString(),
status: cycle?.status,
}; };
const groupedIssues = { const groupedIssues = {
@ -72,7 +76,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"), ...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
}; };
const { reset, watch, control } = useForm({ const { reset } = useForm({
defaultValues, defaultValues,
}); });
@ -118,32 +122,18 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
<> <>
<div className="flex gap-1 text-sm my-2"> <div className="flex gap-1 text-sm my-2">
<div className="flex items-center "> <div className="flex items-center ">
<Controller
control={control}
name="status"
render={({ field: { value } }) => (
<CustomSelect
label={
<span <span
className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`} className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`}
> >
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" /> <Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
{watch("status")} {cycleStatus === "current"
? "In Progress"
: cycleStatus === "completed"
? "Completed"
: cycleStatus === "upcoming"
? "Upcoming"
: "Draft"}
</span> </span>
}
value={value}
onChange={(value: any) => {
submitChanges({ status: value });
}}
>
{CYCLE_STATUS.map((option) => (
<CustomSelect.Option key={option.value} value={option.value}>
<span className="text-xs">{option.label}</span>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div> </div>
<div className="flex justify-center items-center gap-2 rounded-md border bg-transparent h-full p-2 px-4 text-xs font-medium text-gray-900 hover:bg-gray-100 hover:text-gray-900 focus:outline-none"> <div className="flex justify-center items-center gap-2 rounded-md border bg-transparent h-full p-2 px-4 text-xs font-medium text-gray-900 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
<Popover className="flex justify-center items-center relative rounded-lg"> <Popover className="flex justify-center items-center relative rounded-lg">
@ -289,14 +279,16 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
</div> </div>
) : ( ) : (
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white"> <div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
{cycle.owned_by?.first_name && cycle.owned_by.first_name !== "" {cycle.owned_by &&
? cycle.owned_by.first_name.charAt(0) cycle.owned_by?.first_name &&
cycle.owned_by?.first_name !== ""
? cycle.owned_by?.first_name.charAt(0)
: cycle.owned_by?.email.charAt(0)} : cycle.owned_by?.email.charAt(0)}
</div> </div>
))} ))}
{cycle.owned_by.first_name !== "" {cycle.owned_by?.first_name !== ""
? cycle.owned_by.first_name ? cycle.owned_by?.first_name
: cycle.owned_by.email} : cycle.owned_by?.email}
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center py-2"> <div className="flex flex-wrap items-center py-2">

View File

@ -44,7 +44,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
return ( return (
<Popover className="relative z-[1]" ref={ref}> <Popover className="relative z-[1]" ref={ref}>
<Popover.Button <Popover.Button
className="rounded-md border border-gray-300 p-2 outline-none sm:text-sm" className="rounded-full bg-gray-100 p-2 outline-none sm:text-sm"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
> >
{label} {label}

View File

@ -0,0 +1,20 @@
import React from "react";
import type { Props } from "./types";
export const AssignmentClipboardIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.125 19.25C1.74306 19.25 1.4184 19.1163 1.15104 18.8489C0.883681 18.5816 0.75 18.2569 0.75 17.875V4.12499C0.75 3.74305 0.883681 3.41839 1.15104 3.15103C1.4184 2.88367 1.74306 2.74999 2.125 2.74999H6.82292C6.89931 2.21527 7.14375 1.77603 7.55625 1.43228C7.96875 1.08853 8.45 0.916656 9 0.916656C9.55 0.916656 10.0312 1.08853 10.4438 1.43228C10.8562 1.77603 11.1007 2.21527 11.1771 2.74999H15.875C16.2569 2.74999 16.5816 2.88367 16.849 3.15103C17.1163 3.41839 17.25 3.74305 17.25 4.12499V17.875C17.25 18.2569 17.1163 18.5816 16.849 18.8489C16.5816 19.1163 16.2569 19.25 15.875 19.25H2.125ZM2.125 17.875H15.875V4.12499H2.125V17.875ZM4.41667 15.5833H10.6729V14.2083H4.41667V15.5833ZM4.41667 11.6875H13.5833V10.3125H4.41667V11.6875ZM4.41667 7.79166H13.5833V6.41666H4.41667V7.79166ZM9 3.73541C9.21389 3.73541 9.40104 3.6552 9.56146 3.49478C9.72188 3.33436 9.80208 3.14721 9.80208 2.93332C9.80208 2.71943 9.72188 2.53228 9.56146 2.37186C9.40104 2.21145 9.21389 2.13124 9 2.13124C8.78611 2.13124 8.59896 2.21145 8.43854 2.37186C8.27812 2.53228 8.19792 2.71943 8.19792 2.93332C8.19792 3.14721 8.27812 3.33436 8.43854 3.49478C8.59896 3.6552 8.78611 3.73541 9 3.73541ZM2.125 17.875V4.12499V17.875Z" />
</svg>
);

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 = "#858e96",
}) => (
<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,78 @@
import React from "react";
import type { Props } from "./types";
export const CancelledStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#f2655a",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,32.44q9.54,9.75,19.09,19.48"
/>
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,51.92,51.73,32.44"
/>
</g>
</g>
</svg>
);

View File

@ -0,0 +1,69 @@
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}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M30.45,43.75l6.61,6.61L53.92,34"
/>
</g>
</g>
</svg>
);

View File

@ -0,0 +1,25 @@
import React from "react";
import type { Props } from "./types";
export const ContrastIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="9" cy="9" r="5.4375" stroke={color} strokeLinecap="round" />
<path
fill={color}
d="M9 5.81247C9 5.39825 9.33876 5.05526 9.74548 5.13368C10.0057 5.18385 10.2608 5.26029 10.5068 5.36219C10.9845 5.56007 11.4186 5.8501 11.7842 6.21574C12.1499 6.58137 12.4399 7.01543 12.6378 7.49315C12.8357 7.97087 12.9375 8.48289 12.9375 8.99997C12.9375 9.51705 12.8357 10.0291 12.6378 10.5068C12.4399 10.9845 12.1499 11.4186 11.7842 11.7842C11.4186 12.1498 10.9845 12.4399 10.5068 12.6377C10.2608 12.7396 10.0057 12.8161 9.74548 12.8663C9.33876 12.9447 9 12.6017 9 12.1875L9 5.81247Z"
/>
</svg>
);

View File

@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const GridViewIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M2.125 8.3125C1.74688 8.3125 1.42318 8.17786 1.15391 7.90859C0.884635 7.63932 0.75 7.31563 0.75 6.9375V2.125C0.75 1.74688 0.884635 1.42318 1.15391 1.15391C1.42318 0.884635 1.74688 0.75 2.125 0.75H6.9375C7.31563 0.75 7.63932 0.884635 7.90859 1.15391C8.17786 1.42318 8.3125 1.74688 8.3125 2.125V6.9375C8.3125 7.31563 8.17786 7.63932 7.90859 7.90859C7.63932 8.17786 7.31563 8.3125 6.9375 8.3125H2.125ZM2.125 17.25C1.74688 17.25 1.42318 17.1154 1.15391 16.8461C0.884635 16.5768 0.75 16.2531 0.75 15.875V11.0625C0.75 10.6844 0.884635 10.3607 1.15391 10.0914C1.42318 9.82214 1.74688 9.6875 2.125 9.6875H6.9375C7.31563 9.6875 7.63932 9.82214 7.90859 10.0914C8.17786 10.3607 8.3125 10.6844 8.3125 11.0625V15.875C8.3125 16.2531 8.17786 16.5768 7.90859 16.8461C7.63932 17.1154 7.31563 17.25 6.9375 17.25H2.125ZM11.0625 8.3125C10.6844 8.3125 10.3607 8.17786 10.0914 7.90859C9.82214 7.63932 9.6875 7.31563 9.6875 6.9375V2.125C9.6875 1.74688 9.82214 1.42318 10.0914 1.15391C10.3607 0.884635 10.6844 0.75 11.0625 0.75H15.875C16.2531 0.75 16.5768 0.884635 16.8461 1.15391C17.1154 1.42318 17.25 1.74688 17.25 2.125V6.9375C17.25 7.31563 17.1154 7.63932 16.8461 7.90859C16.5768 8.17786 16.2531 8.3125 15.875 8.3125H11.0625ZM11.0625 17.25C10.6844 17.25 10.3607 17.1154 10.0914 16.8461C9.82214 16.5768 9.6875 16.2531 9.6875 15.875V11.0625C9.6875 10.6844 9.82214 10.3607 10.0914 10.0914C10.3607 9.82214 10.6844 9.6875 11.0625 9.6875H15.875C16.2531 9.6875 16.5768 9.82214 16.8461 10.0914C17.1154 10.3607 17.25 10.6844 17.25 11.0625V15.875C17.25 16.2531 17.1154 16.5768 16.8461 16.8461C16.5768 17.1154 16.2531 17.25 15.875 17.25H11.0625ZM2.125 6.9375H6.9375V2.125H2.125V6.9375ZM11.0625 6.9375H15.875V2.125H11.0625V6.9375ZM11.0625 15.875H15.875V11.0625H11.0625V15.875ZM2.125 15.875H6.9375V11.0625H2.125V15.875Z"
/>
</svg>
);

View File

@ -2,21 +2,26 @@ import React from "react";
import type { Props } from "./types"; import type { Props } from "./types";
export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => ( export const HeartbeatIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg <svg
width={width} width={width}
height={height} height={height}
className={className} className={className}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M3 12H7.5L9 6L13 18L15 9L16.5 12H21" d="M2 8H5L6 4L8.66667 12L10 6L11 8H14"
stroke="black" stroke={color}
strokeWidth="1.5" strokeWidth="1.5"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
); );

View File

@ -1,12 +1,15 @@
export * from "./attachment-icon"; export * from "./attachment-icon";
export * from "./backlog-state-icon";
export * from "./blocked-icon"; export * from "./blocked-icon";
export * from "./blocker-icon"; export * from "./blocker-icon";
export * from "./bolt-icon"; export * from "./bolt-icon";
export * from "./calendar-month-icon"; export * from "./calendar-month-icon";
export * from "./cancel-icon"; export * from "./cancel-icon";
export * from "./cancelled-state-icon";
export * from "./clipboard-icon"; export * from "./clipboard-icon";
export * from "./comment-icon"; export * from "./comment-icon";
export * from "./completed-cycle-icon"; export * from "./completed-cycle-icon";
export * from "./completed-state-icon";
export * from "./current-cycle-icon"; export * from "./current-cycle-icon";
export * from "./cycle-icon"; export * from "./cycle-icon";
export * from "./discord-icon"; export * from "./discord-icon";
@ -16,6 +19,7 @@ export * from "./ellipsis-horizontal-icon";
export * from "./external-link-icon"; export * from "./external-link-icon";
export * from "./github-icon"; export * from "./github-icon";
export * from "./heartbeat-icon"; export * from "./heartbeat-icon";
export * from "./started-state-icon";
export * from "./layer-diagonal-icon"; export * from "./layer-diagonal-icon";
export * from "./lock-icon"; export * from "./lock-icon";
export * from "./menu-icon"; export * from "./menu-icon";
@ -23,9 +27,17 @@ export * from "./plus-icon";
export * from "./question-mark-circle-icon"; export * from "./question-mark-circle-icon";
export * from "./setting-icon"; export * from "./setting-icon";
export * from "./signal-cellular-icon"; export * from "./signal-cellular-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./tag-icon"; export * from "./tag-icon";
export * from "./tune-icon"; export * from "./tune-icon";
export * from "./unstarted-state-icon";
export * from "./upcoming-cycle-icon"; export * from "./upcoming-cycle-icon";
export * from "./user-group-icon"; export * from "./user-group-icon";
export * from "./user-icon-circle"; export * from "./user-icon-circle";
export * from "./user-icon"; export * from "./user-icon";
export * from "./grid-view-icons";
export * from "./assignment-clipboard-icon";
export * from "./tick-mark-icon";
export * from "./contrast-icon";
export * from "./people-group-icon";

View File

@ -6,19 +6,21 @@ export const LayerDiagonalIcon: React.FC<Props> = ({
width = "24", width = "24",
height = "24", height = "24",
className, className,
color = "black", color = "#858E96",
}) => ( }) => (
<svg <svg
width={width} width={width}
height={height} height={height}
className={className} className={className}
viewBox="0 0 24 24" viewBox="0 0 16 16"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M12.6004 4.20111C12.6005 4.10308 12.5766 4.00653 12.5307 3.91989C12.4849 3.83325 12.4185 3.75916 12.3374 3.7041C12.2563 3.64905 12.1629 3.6147 12.0655 3.60407C11.968 3.59343 11.8695 3.60684 11.7784 3.64311L4.73799 6.43191C4.4024 6.56473 4.11449 6.79536 3.91163 7.09387C3.70877 7.39239 3.60033 7.74499 3.60039 8.10591V14.9963C3.60037 15.0942 3.6243 15.1906 3.67009 15.2771C3.71587 15.3636 3.78212 15.4376 3.86306 15.4926C3.94401 15.5476 4.03718 15.582 4.13446 15.5928C4.23175 15.6035 4.33018 15.5903 4.42119 15.5543L4.80039 15.4043V16.6943C4.52882 16.7903 4.23817 16.8198 3.95286 16.7802C3.66755 16.7405 3.39592 16.633 3.16077 16.4667C2.92563 16.3003 2.73385 16.0799 2.60153 15.8241C2.46922 15.5682 2.40024 15.2844 2.40039 14.9963V8.10591C2.40029 7.50438 2.58101 6.91671 2.91912 6.41919C3.25722 5.92166 3.73707 5.53727 4.29639 5.31591L11.3368 2.52711C11.6011 2.42232 11.8864 2.38163 12.1695 2.40837C12.4526 2.43511 12.7252 2.52852 12.9652 2.68095C13.2052 2.83337 13.4057 3.04049 13.5503 3.28532C13.6948 3.53015 13.7793 3.80574 13.7968 4.08951L12.5992 4.56231V4.20111H12.6004ZM16.2004 6.60111C16.2003 6.50318 16.1762 6.40677 16.1303 6.32029C16.0844 6.23381 16.018 6.15988 15.9369 6.10496C15.8558 6.05005 15.7625 6.01581 15.6652 6.00523C15.5678 5.99466 15.4694 6.00808 15.3784 6.04431L7.95879 8.98311C7.73506 9.07165 7.54312 9.22541 7.40788 9.42442C7.27264 9.62343 7.20035 9.8585 7.20039 10.0991V17.3975C7.20037 17.4954 7.2243 17.5918 7.27009 17.6783C7.31587 17.7648 7.38212 17.8388 7.46306 17.8938C7.54401 17.9488 7.63718 17.9832 7.73446 17.994C7.83175 18.0047 7.93018 17.9915 8.02119 17.9555L9.60039 17.3279V18.6191L8.46399 19.0691C8.1911 19.1773 7.89587 19.2172 7.60405 19.1852C7.31223 19.1531 7.03267 19.0502 6.78974 18.8854C6.54681 18.7206 6.34788 18.4988 6.2103 18.2395C6.07272 17.9801 6.00065 17.6911 6.00039 17.3975V10.0979C6.00031 9.61668 6.14489 9.14655 6.41537 8.74853C6.68585 8.35051 7.06974 8.043 7.51719 7.86591L14.9368 4.92831C15.2096 4.82033 15.5047 4.78068 15.7964 4.81283C16.088 4.84497 16.3674 4.94793 16.6102 5.11273C16.8529 5.27753 17.0517 5.49919 17.1893 5.75839C17.3268 6.01759 17.3989 6.30648 17.3992 6.59991V6.72351L16.2004 7.19991V6.59991V6.60111ZM19.5796 8.44311C19.6705 8.40713 19.7688 8.39391 19.866 8.4046C19.9632 8.4153 20.0563 8.44958 20.1372 8.50447C20.2181 8.55936 20.2844 8.63319 20.3303 8.71954C20.3761 8.80589 20.4002 8.90213 20.4004 8.99991V16.9487C20.4002 17.0688 20.3639 17.1861 20.2963 17.2853C20.2287 17.3846 20.1329 17.4613 20.0212 17.5055L12.8212 20.3579C12.7302 20.3939 12.6317 20.4071 12.5345 20.3964C12.4372 20.3856 12.344 20.3512 12.2631 20.2962C12.1821 20.2412 12.1159 20.1672 12.0701 20.0807C12.0243 19.9942 12.0004 19.8978 12.0004 19.7999V11.8523C12.0004 11.732 12.0365 11.6145 12.1041 11.515C12.1718 11.4155 12.2677 11.3386 12.3796 11.2943L19.5796 8.44311ZM21.6004 8.99991C21.6002 8.70638 21.5283 8.41735 21.3909 8.15799C21.2535 7.89863 21.0547 7.67681 20.8119 7.51187C20.5691 7.34693 20.2896 7.24386 19.9979 7.21166C19.7061 7.17946 19.4109 7.21909 19.138 7.32711L11.938 10.1783C11.6024 10.3111 11.3145 10.5418 11.1116 10.8403C10.9088 11.1388 10.8003 11.4914 10.8004 11.8523V19.7999C10.8003 20.0935 10.8721 20.3827 11.0095 20.6422C11.1468 20.9018 11.3456 21.1237 11.5884 21.2888C11.8312 21.4539 12.1108 21.5571 12.4026 21.5893C12.6945 21.6216 12.9898 21.582 13.2628 21.4739L20.4628 18.6215C20.7982 18.4888 21.086 18.2583 21.2888 17.96C21.4917 17.6618 21.6002 17.3094 21.6004 16.9487V8.99991Z" d="M8.40034 2.80076C8.40041 2.73541 8.38446 2.67104 8.35389 2.61328C8.32332 2.55552 8.27907 2.50613 8.225 2.46942C8.17093 2.43272 8.1087 2.40982 8.04373 2.40273C7.97877 2.39564 7.91306 2.40458 7.85234 2.42876L3.15874 4.28796C2.93501 4.3765 2.74307 4.53026 2.60783 4.72927C2.47259 4.92828 2.4003 5.16335 2.40034 5.40396V9.99756C2.40033 10.0628 2.41628 10.1271 2.44681 10.1847C2.47733 10.2424 2.5215 10.2917 2.57546 10.3284C2.62942 10.3651 2.69154 10.388 2.75639 10.3952C2.82125 10.4024 2.88687 10.3936 2.94754 10.3696L3.20034 10.2696V11.1296C3.01929 11.1936 2.82553 11.2132 2.63532 11.1868C2.44512 11.1604 2.26403 11.0887 2.10726 10.9778C1.9505 10.8669 1.82265 10.72 1.73444 10.5494C1.64623 10.3788 1.60024 10.1896 1.60034 9.99756V5.40396C1.60027 5.00294 1.72076 4.61116 1.94616 4.27948C2.17156 3.9478 2.49146 3.69153 2.86434 3.54396L7.55794 1.68476C7.73414 1.6149 7.92438 1.58777 8.11308 1.6056C8.30178 1.62343 8.48358 1.6857 8.64358 1.78732C8.80358 1.88894 8.93723 2.02701 9.03359 2.19023C9.12994 2.35345 9.18627 2.53718 9.19794 2.72636L8.39954 3.04156V2.80076H8.40034ZM10.8003 4.40076C10.8003 4.33548 10.7842 4.2712 10.7536 4.21355C10.723 4.15589 10.6787 4.10661 10.6247 4.07C10.5706 4.03339 10.5084 4.01056 10.4435 4.00351C10.3786 3.99646 10.313 4.0054 10.2523 4.02956L5.30594 5.98876C5.15679 6.04779 5.02883 6.15029 4.93867 6.28297C4.84851 6.41564 4.80031 6.57235 4.80034 6.73276V11.5984C4.80033 11.6636 4.81628 11.7279 4.84681 11.7855C4.87733 11.8432 4.9215 11.8925 4.97546 11.9292C5.02942 11.9659 5.09154 11.9888 5.15639 11.996C5.22125 12.0032 5.28687 11.9944 5.34754 11.9704L6.40034 11.552V12.4128L5.64274 12.7128C5.46081 12.7849 5.264 12.8115 5.06945 12.7901C4.8749 12.7688 4.68853 12.7002 4.52658 12.5903C4.36462 12.4804 4.232 12.3326 4.14028 12.1597C4.04856 11.9868 4.00052 11.7941 4.00034 11.5984V6.73196C4.00029 6.41114 4.09668 6.09772 4.277 5.83237C4.45732 5.56703 4.71324 5.36202 5.01154 5.24396L9.95794 3.28556C10.1398 3.21357 10.3366 3.18714 10.531 3.20857C10.7254 3.23 10.9117 3.29864 11.0735 3.40851C11.2354 3.51838 11.3679 3.66615 11.4596 3.83895C11.5513 4.01175 11.5993 4.20434 11.5995 4.39996V4.48236L10.8003 4.79996V4.39996V4.40076ZM13.0531 5.62876C13.1138 5.60477 13.1793 5.59596 13.2441 5.60309C13.3089 5.61022 13.371 5.63307 13.4249 5.66967C13.4788 5.70626 13.523 5.75548 13.5536 5.81305C13.5842 5.87061 13.6002 5.93478 13.6003 5.99996V11.2992C13.6002 11.3792 13.576 11.4574 13.531 11.5236C13.4859 11.5898 13.422 11.6409 13.3475 11.6704L8.54754 13.572C8.48687 13.596 8.42125 13.6048 8.35639 13.5976C8.29154 13.5904 8.22942 13.5675 8.17546 13.5308C8.1215 13.4941 8.07733 13.4448 8.04681 13.3871C8.01628 13.3295 8.00033 13.2652 8.00034 13.2V7.90156C8.00033 7.82136 8.02443 7.743 8.06951 7.67666C8.11459 7.61033 8.17857 7.55907 8.25314 7.52956L13.0531 5.62876V5.62876ZM14.4003 5.99996C14.4002 5.80428 14.3523 5.61159 14.2607 5.43868C14.1691 5.26577 14.0365 5.11789 13.8747 5.00793C13.7128 4.89797 13.5265 4.82926 13.332 4.80779C13.1375 4.78633 12.9407 4.81275 12.7587 4.88476L7.95874 6.78556C7.73502 6.8741 7.54307 7.02786 7.40783 7.22687C7.27259 7.42588 7.2003 7.66095 7.20034 7.90156V13.2C7.20031 13.3957 7.24816 13.5885 7.33973 13.7615C7.4313 13.9345 7.56381 14.0825 7.72569 14.1926C7.88758 14.3026 8.07393 14.3714 8.26849 14.3929C8.46306 14.4144 8.65993 14.388 8.84194 14.316L13.6419 12.4144C13.8655 12.3259 14.0574 12.1722 14.1926 11.9734C14.3279 11.7745 14.4002 11.5396 14.4003 11.2992V5.99996Z"
fill={color} fill={color}
stroke={color}
strokeWidth="0.25"
/> />
</svg> </svg>
); );

View File

@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const PeopleGroupIcon: React.FC<Props> = ({
width = "24",
height = "16",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M12.2951 6.66667C13.1001 6.66667 13.7534 7.18933 13.7534 7.83333V10.9993C13.7534 11.7952 13.3582 12.5584 12.6548 13.1211C11.9514 13.6839 10.9974 14 10.0026 14C9.0078 14 8.05376 13.6839 7.35034 13.1211C6.64692 12.5584 6.25175 11.7952 6.25175 10.9993V7.83333C6.25175 7.18933 6.90425 6.66667 7.71008 6.66667H12.2951ZM12.2951 7.66667H7.71008C7.65483 7.66667 7.60184 7.68423 7.56277 7.71548C7.5237 7.74674 7.50175 7.78913 7.50175 7.83333V10.9993C7.50175 11.5299 7.76523 12.0388 8.23422 12.414C8.70322 12.7892 9.33932 13 10.0026 13C10.6658 13 11.3019 12.7892 11.7709 12.414C12.2399 12.0388 12.5034 11.5299 12.5034 10.9993V7.83333C12.5034 7.78913 12.4815 7.74674 12.4424 7.71548C12.4033 7.68423 12.3503 7.66667 12.2951 7.66667ZM3.12508 6.66667H5.94258C5.64858 6.95073 5.46903 7.29937 5.42758 7.66667H3.12508C3.06983 7.66667 3.01684 7.68423 2.97777 7.71548C2.9387 7.74674 2.91675 7.78913 2.91675 7.83333V9.99933C2.91669 10.2513 2.988 10.4999 3.12531 10.7266C3.26262 10.9533 3.46236 11.1522 3.70952 11.3083C3.95669 11.4644 4.24486 11.5737 4.55239 11.6279C4.85991 11.6821 5.17879 11.6799 5.48508 11.6213C5.55591 11.9573 5.68508 12.278 5.86258 12.576C5.36865 12.6817 4.85094 12.6951 4.34949 12.6152C3.84804 12.5353 3.37629 12.3642 2.9707 12.1151C2.56512 11.866 2.23657 11.5457 2.01047 11.1788C1.78436 10.8119 1.66676 10.4084 1.66675 9.99933V7.83333C1.66675 7.18933 2.32008 6.66667 3.12508 6.66667ZM14.0626 6.66667H16.8751C17.6801 6.66667 18.3334 7.18933 18.3334 7.83333V10C18.3335 10.4088 18.2162 10.8121 17.9904 11.1788C17.7646 11.5455 17.4365 11.8658 17.0314 12.1149C16.6262 12.364 16.155 12.5353 15.6539 12.6155C15.1529 12.6956 14.6355 12.6826 14.1417 12.5773C14.3201 12.2787 14.4492 11.958 14.5209 11.622C14.8268 11.6798 15.1451 11.6815 15.452 11.627C15.7588 11.5725 16.0463 11.4631 16.2928 11.307C16.5393 11.151 16.7384 10.9524 16.8754 10.726C17.0123 10.4997 17.0834 10.2515 17.0834 10V7.83333C17.0834 7.78913 17.0615 7.74674 17.0224 7.71548C16.9833 7.68423 16.9303 7.66667 16.8751 7.66667H14.5776C14.5361 7.29937 14.3566 6.95073 14.0626 6.66667ZM10.0001 2C10.6631 2 11.299 2.21071 11.7678 2.58579C12.2367 2.96086 12.5001 3.46957 12.5001 4C12.5001 4.53043 12.2367 5.03914 11.7678 5.41421C11.299 5.78929 10.6631 6 10.0001 6C9.33704 6 8.70115 5.78929 8.23231 5.41421C7.76347 5.03914 7.50008 4.53043 7.50008 4C7.50008 3.46957 7.76347 2.96086 8.23231 2.58579C8.70115 2.21071 9.33704 2 10.0001 2ZM15.4167 2.66667C15.9693 2.66667 16.4992 2.84226 16.8899 3.15482C17.2806 3.46738 17.5001 3.89131 17.5001 4.33333C17.5001 4.77536 17.2806 5.19928 16.8899 5.51184C16.4992 5.82441 15.9693 6 15.4167 6C14.8642 6 14.3343 5.82441 13.9436 5.51184C13.5529 5.19928 13.3334 4.77536 13.3334 4.33333C13.3334 3.89131 13.5529 3.46738 13.9436 3.15482C14.3343 2.84226 14.8642 2.66667 15.4167 2.66667ZM4.58341 2.66667C5.13595 2.66667 5.66585 2.84226 6.05655 3.15482C6.44725 3.46738 6.66675 3.89131 6.66675 4.33333C6.66675 4.77536 6.44725 5.19928 6.05655 5.51184C5.66585 5.82441 5.13595 6 4.58341 6C4.03088 6 3.50098 5.82441 3.11028 5.51184C2.71957 5.19928 2.50008 4.77536 2.50008 4.33333C2.50008 3.89131 2.71957 3.46738 3.11028 3.15482C3.50098 2.84226 4.03088 2.66667 4.58341 2.66667ZM10.0001 3C9.66856 3 9.35062 3.10536 9.1162 3.29289C8.88178 3.48043 8.75008 3.73478 8.75008 4C8.75008 4.26522 8.88178 4.51957 9.1162 4.70711C9.35062 4.89464 9.66856 5 10.0001 5C10.3316 5 10.6495 4.89464 10.884 4.70711C11.1184 4.51957 11.2501 4.26522 11.2501 4C11.2501 3.73478 11.1184 3.48043 10.884 3.29289C10.6495 3.10536 10.3316 3 10.0001 3ZM15.4167 3.66667C15.1957 3.66667 14.9838 3.7369 14.8275 3.86193C14.6712 3.98695 14.5834 4.15652 14.5834 4.33333C14.5834 4.51014 14.6712 4.67971 14.8275 4.80474C14.9838 4.92976 15.1957 5 15.4167 5C15.6378 5 15.8497 4.92976 16.006 4.80474C16.1623 4.67971 16.2501 4.51014 16.2501 4.33333C16.2501 4.15652 16.1623 3.98695 16.006 3.86193C15.8497 3.7369 15.6378 3.66667 15.4167 3.66667ZM4.58341 3.66667C4.3624 3.66667 4.15044 3.7369 3.99416 3.86193C3.83788 3.98695 3.75008 4.15652 3.75008 4.33333C3.75008 4.51014 3.83788 4.67971 3.99416 4.80474C4.15044 4.92976 4.3624 5 4.58341 5C4.80443 5 5.01639 4.92976 5.17267 4.80474C5.32895 4.67971 5.41675 4.51014 5.41675 4.33333C5.41675 4.15652 5.32895 3.98695 5.17267 3.86193C5.01639 3.7369 4.80443 3.66667 4.58341 3.66667Z"
/>
</svg>
);

View File

@ -2,18 +2,23 @@ import React from "react";
import type { Props } from "./types"; import type { Props } from "./types";
export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => ( export const SettingIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "black",
className,
}) => (
<svg <svg
width={width} width={width}
height={height} height={height}
className={className} className={className}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M14.0794 1.5C14.2382 1.50001 14.3929 1.55041 14.5212 1.64394C14.6495 1.73748 14.7448 1.86933 14.7934 2.0205L15.6184 4.584C15.9649 4.7535 16.2964 4.944 16.6129 5.1585L19.2469 4.5915C19.4022 4.55834 19.564 4.57534 19.7091 4.64003C19.8541 4.70473 19.9748 4.81379 20.0539 4.9515L22.1329 8.55C22.2123 8.68763 22.2459 8.84693 22.2289 9.0049C22.212 9.16288 22.1452 9.31139 22.0384 9.429L20.2309 11.424C20.2572 11.8065 20.2572 12.1905 20.2309 12.573L22.0384 14.571C22.1452 14.6886 22.212 14.8371 22.2289 14.9951C22.2459 15.1531 22.2123 15.3124 22.1329 15.45L20.0539 19.05C19.9746 19.1874 19.8538 19.2962 19.7088 19.3606C19.5638 19.425 19.4021 19.4418 19.2469 19.4085L16.6129 18.8415C16.2979 19.0545 15.9649 19.2465 15.6199 19.416L14.7934 21.9795C14.7448 22.1307 14.6495 22.2625 14.5212 22.3561C14.3929 22.4496 14.2382 22.5 14.0794 22.5H9.92141C9.76262 22.5 9.60793 22.4496 9.47962 22.3561C9.35131 22.2625 9.256 22.1307 9.20741 21.9795L8.38391 19.4175C8.03834 19.2485 7.70502 19.0555 7.38641 18.84L4.75391 19.4085C4.5986 19.4417 4.43678 19.4247 4.29176 19.36C4.14673 19.2953 4.02598 19.1862 3.94691 19.0485L1.86791 15.45C1.78852 15.3124 1.75489 15.1531 1.77188 14.9951C1.78886 14.8371 1.85558 14.6886 1.96241 14.571L3.76991 12.573C3.74372 12.1914 3.74372 11.8086 3.76991 11.427L1.96241 9.429C1.85558 9.31139 1.78886 9.16288 1.77188 9.0049C1.75489 8.84693 1.78852 8.68763 1.86791 8.55L3.94691 4.95C4.0262 4.81256 4.14704 4.70381 4.29205 4.63939C4.43706 4.57497 4.59876 4.55821 4.75391 4.5915L7.38641 5.16C7.70441 4.9455 8.03741 4.752 8.38391 4.5825L9.20891 2.0205C9.25734 1.86982 9.3522 1.73832 9.47991 1.64482C9.60762 1.55133 9.76163 1.50064 9.91991 1.5H14.0779H14.0794ZM13.5304 3H10.4704L9.61841 5.6505L9.04391 5.931C8.76148 6.06921 8.48885 6.22657 8.22791 6.402L7.69691 6.762L4.97291 6.174L3.44291 8.826L5.31041 10.893L5.26541 11.529C5.24385 11.8426 5.24385 12.1574 5.26541 12.471L5.31041 13.107L3.43991 15.174L4.97141 17.826L7.69541 17.2395L8.22641 17.598C8.48735 17.7734 8.75998 17.9308 9.04241 18.069L9.61691 18.3495L10.4704 21H13.5334L14.3884 18.348L14.9614 18.069C15.2435 17.9311 15.5157 17.7737 15.7759 17.598L16.3054 17.2395L19.0309 17.826L20.5609 15.174L18.6919 13.107L18.7369 12.471C18.7585 12.1569 18.7585 11.8416 18.7369 11.5275L18.6919 10.8915L20.5624 8.826L19.0309 6.174L16.3054 6.759L15.7759 6.402C15.5157 6.22622 15.2435 6.06884 14.9614 5.931L14.3884 5.652L13.5319 3H13.5304ZM12.0004 7.5C13.1939 7.5 14.3385 7.97411 15.1824 8.81802C16.0263 9.66193 16.5004 10.8065 16.5004 12C16.5004 13.1935 16.0263 14.3381 15.1824 15.182C14.3385 16.0259 13.1939 16.5 12.0004 16.5C10.8069 16.5 9.66234 16.0259 8.81843 15.182C7.97451 14.3381 7.50041 13.1935 7.50041 12C7.50041 10.8065 7.97451 9.66193 8.81843 8.81802C9.66234 7.97411 10.8069 7.5 12.0004 7.5ZM12.0004 9C11.2048 9 10.4417 9.31607 9.87909 9.87868C9.31648 10.4413 9.00041 11.2044 9.00041 12C9.00041 12.7956 9.31648 13.5587 9.87909 14.1213C10.4417 14.6839 11.2048 15 12.0004 15C12.7961 15 13.5591 14.6839 14.1217 14.1213C14.6843 13.5587 15.0004 12.7956 15.0004 12C15.0004 11.2044 14.6843 10.4413 14.1217 9.87868C13.5591 9.31607 12.7961 9 12.0004 9Z" fill={color}
fill="black" d="M12.9062 1.375C13.0518 1.375 13.1936 1.42121 13.3112 1.50695C13.4288 1.59269 13.5162 1.71355 13.5607 1.85212L14.317 4.202C14.6346 4.35737 14.9385 4.532 15.2286 4.72862L17.6431 4.20888C17.7854 4.17848 17.9338 4.19406 18.0667 4.25336C18.1997 4.31267 18.3103 4.41264 18.3828 4.53888L20.2886 7.8375C20.3614 7.96366 20.3922 8.10968 20.3766 8.2545C20.361 8.39931 20.2999 8.53544 20.202 8.64325L18.5451 10.472C18.5692 10.8227 18.5692 11.1746 18.5451 11.5252L20.202 13.3567C20.2999 13.4646 20.361 13.6007 20.3766 13.7455C20.3922 13.8903 20.3614 14.0363 20.2886 14.1625L18.3828 17.4625C18.3101 17.5885 18.1994 17.6882 18.0664 17.7472C17.9335 17.8063 17.7853 17.8216 17.6431 17.7911L15.2286 17.2714C14.9398 17.4666 14.6346 17.6426 14.3183 17.798L13.5607 20.1479C13.5162 20.2864 13.4288 20.4073 13.3112 20.4931C13.1936 20.5788 13.0518 20.625 12.9062 20.625H9.0947C8.94915 20.625 8.80735 20.5788 8.68973 20.4931C8.57211 20.4073 8.48474 20.2864 8.4402 20.1479L7.68533 17.7994C7.36856 17.6445 7.06302 17.4676 6.77095 17.27L4.35783 17.7911C4.21547 17.8215 4.06713 17.8059 3.93419 17.7466C3.80125 17.6873 3.69057 17.5874 3.61808 17.4611L1.71233 14.1625C1.63956 14.0363 1.60873 13.8903 1.6243 13.7455C1.63987 13.6007 1.70103 13.4646 1.79895 13.3567L3.45583 11.5252C3.43183 11.1755 3.43183 10.8245 3.45583 10.4748L1.79895 8.64325C1.70103 8.53544 1.63987 8.39931 1.6243 8.2545C1.60873 8.10968 1.63956 7.96366 1.71233 7.8375L3.61808 4.5375C3.69077 4.41151 3.80154 4.31183 3.93446 4.25278C4.06739 4.19373 4.21562 4.17836 4.35783 4.20888L6.77095 4.73C7.06245 4.53338 7.3677 4.356 7.68533 4.20063L8.44158 1.85212C8.48597 1.714 8.57293 1.59346 8.69 1.50776C8.80706 1.42205 8.94824 1.37559 9.09333 1.375H12.9048H12.9062ZM12.403 2.75H9.59795L8.81695 5.17963L8.29033 5.43675C8.03144 5.56344 7.78152 5.70769 7.54233 5.8685L7.05558 6.1985L4.55858 5.6595L3.15608 8.0905L4.86795 9.98525L4.8267 10.5682C4.80695 10.8557 4.80695 11.1443 4.8267 11.4318L4.86795 12.0147L3.15333 13.9095L4.5572 16.3405L7.0542 15.8029L7.54095 16.1315C7.78015 16.2923 8.03007 16.4366 8.28895 16.5632L8.81558 16.8204L9.59795 19.25H12.4057L13.1895 16.819L13.7147 16.5632C13.9733 16.4369 14.2228 16.2926 14.4613 16.1315L14.9467 15.8029L17.4451 16.3405L18.8476 13.9095L17.1343 12.0147L17.1756 11.4318C17.1954 11.1438 17.1954 10.8548 17.1756 10.5669L17.1343 9.98388L18.849 8.0905L17.4451 5.6595L14.9467 6.19575L14.4613 5.8685C14.2228 5.70737 13.9733 5.5631 13.7147 5.43675L13.1895 5.181L12.4043 2.75H12.403ZM11.0005 6.875C12.0945 6.875 13.1437 7.3096 13.9173 8.08318C14.6909 8.85677 15.1255 9.90598 15.1255 11C15.1255 12.094 14.6909 13.1432 13.9173 13.9168C13.1437 14.6904 12.0945 15.125 11.0005 15.125C9.90644 15.125 8.85723 14.6904 8.08364 13.9168C7.31005 13.1432 6.87545 12.094 6.87545 11C6.87545 9.90598 7.31005 8.85677 8.08364 8.08318C8.85723 7.3096 9.90644 6.875 11.0005 6.875ZM11.0005 8.25C10.2711 8.25 9.57164 8.53973 9.05591 9.05546C8.54018 9.57118 8.25045 10.2707 8.25045 11C8.25045 11.7293 8.54018 12.4288 9.05591 12.9445C9.57164 13.4603 10.2711 13.75 11.0005 13.75C11.7298 13.75 12.4293 13.4603 12.945 12.9445C13.4607 12.4288 13.7505 11.7293 13.7505 11C13.7505 10.2707 13.4607 9.57118 12.945 9.05546C12.4293 8.53973 11.7298 8.25 11.0005 8.25Z"
/> />
</svg> </svg>
); );

View File

@ -0,0 +1,77 @@
import React from "react";
import type { Props } from "./types";
export const StartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#fbb040",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 83.36 83.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20,7.19a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.17,20a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.42,76.17A39.78,39.78,0,0,1,20,75.64"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.19,63.42A39.75,39.75,0,0,1,7.73,20"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.21q9.57-14.45,19.13-28.9a35.8,35.8,0,0,0-39.09,0Z"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.7,61.45,70.6a35.75,35.75,0,0,1-39.09,0Z"
/>
</g>
</g>
</svg>
);

View File

@ -0,0 +1,29 @@
import {
BacklogStateIcon,
CancelledStateIcon,
CompletedStateIcon,
StartedStateIcon,
UnstartedStateIcon,
} from "components/icons";
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 <UnstartedStateIcon 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 <CancelledStateIcon width={width} height={height} color={color} />;
default:
return <></>;
}
};

View File

@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const TickMarkIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M19.0653 7.06348C19.2051 7.52373 19.3125 8.00065 19.3875 8.49423C19.4625 8.98783 19.5 9.48976 19.5 10C19.5 11.3333 19.2544 12.5779 18.7634 13.7337C18.2724 14.8894 17.5977 15.8964 16.7394 16.7548C15.881 17.6131 14.874 18.2852 13.7182 18.7711C12.5625 19.257 11.323 19.5 9.99998 19.5C8.66664 19.5 7.42209 19.257 6.26633 18.7711C5.11058 18.2852 4.10353 17.6131 3.2452 16.7548C2.38687 15.8964 1.71475 14.8894 1.22885 13.7337C0.74295 12.5779 0.5 11.3333 0.5 10C0.5 8.67694 0.74295 7.43752 1.22885 6.28176C1.71475 5.12599 2.38687 4.11894 3.2452 3.26061C4.10353 2.40227 5.11058 1.7276 6.26633 1.23658C7.42209 0.745547 8.66664 0.500031 9.99998 0.500031C11.0577 0.500031 12.0529 0.656764 12.9856 0.970231C13.9182 1.2837 14.7852 1.7103 15.5865 2.25003C15.7186 2.34106 15.7939 2.46254 15.8125 2.61446C15.8311 2.76637 15.7865 2.90643 15.6788 3.03463C15.5775 3.15643 15.4477 3.22598 15.2894 3.24328C15.1311 3.2606 14.9827 3.22374 14.8442 3.13271C14.1506 2.67374 13.3965 2.30931 12.5817 2.03943C11.767 1.76956 10.9064 1.63463 9.99998 1.63463C7.64101 1.63463 5.65703 2.43911 4.04805 4.04808C2.43908 5.65706 1.6346 7.64104 1.6346 10C1.6346 12.359 2.43908 14.3429 4.04805 15.9519C5.65703 17.5609 7.64101 18.3654 9.99998 18.3654C12.3589 18.3654 14.3429 17.5609 15.9519 15.9519C17.5609 14.3429 18.3654 12.359 18.3654 10C18.3654 9.56411 18.3371 9.14487 18.2807 8.74231C18.2243 8.33974 18.1397 7.94038 18.0269 7.54423C17.9897 7.40833 17.9942 7.26603 18.0404 7.11733C18.0865 6.96861 18.1724 6.86221 18.298 6.79811C18.4429 6.70836 18.5919 6.6856 18.7451 6.72983C18.8983 6.77405 19.0051 6.88526 19.0653 7.06348ZM8.0192 13.7077L5.12308 10.7962C5.01153 10.6846 4.95736 10.5462 4.96058 10.3808C4.96378 10.2154 5.02628 10.0718 5.14808 9.95001C5.26601 9.83207 5.40607 9.77311 5.56825 9.77311C5.73042 9.77311 5.87561 9.83207 6.00383 9.95001L8.52498 12.4962L18.2384 2.78273C18.35 2.67118 18.4868 2.61445 18.649 2.61253C18.8112 2.61061 18.9564 2.67055 19.0846 2.79233C19.2128 2.92053 19.2769 3.06572 19.2769 3.22791C19.2769 3.39007 19.2128 3.53526 19.0846 3.66348L9.03075 13.7077C8.88715 13.8577 8.71599 13.9327 8.51728 13.9327C8.31856 13.9327 8.15253 13.8577 8.0192 13.7077Z"
/>
</svg>
);

View File

@ -0,0 +1,59 @@
import React from "react";
import type { Props } from "./types";
export const UnstartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#858e96",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
</g>
</g>
</svg>
);

View File

@ -14,8 +14,8 @@ import {
IssuePrioritySelect, IssuePrioritySelect,
IssueProjectSelect, IssueProjectSelect,
IssueStateSelect, IssueStateSelect,
IssueDateSelect,
} from "components/issues/select"; } from "components/issues/select";
import { CycleSelect as IssueCycleSelect } from "components/cycles/select";
import { CreateStateModal } from "components/states"; import { CreateStateModal } from "components/states";
import { CreateUpdateCycleModal } from "components/cycles"; import { CreateUpdateCycleModal } from "components/cycles";
import { CreateLabelModal } from "components/labels"; import { CreateLabelModal } from "components/labels";
@ -266,16 +266,16 @@ export const IssueForm: FC<IssueFormProps> = ({
/> />
<Controller <Controller
control={control} control={control}
name="cycle" name="priority"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<IssueCycleSelect projectId={projectId} value={value} onChange={onChange} /> <IssuePrioritySelect value={value} onChange={onChange} />
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="priority" name="assignees"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} /> <IssueAssigneeSelect projectId={projectId} value={value} onChange={onChange} />
)} )}
/> />
<Controller <Controller
@ -295,21 +295,10 @@ export const IssueForm: FC<IssueFormProps> = ({
control={control} control={control}
name="target_date" name="target_date"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomDatePicker <IssueDateSelect value={value} onChange={onChange} />
value={value}
onChange={onChange}
className="max-w-[7rem]"
/>
)} )}
/> />
</div> </div>
<Controller
control={control}
name="assignees"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect projectId={projectId} value={value} onChange={onChange} />
)}
/>
<IssueParentSelect <IssueParentSelect
control={control} control={control}
isOpen={parentIssueListModalOpen} isOpen={parentIssueListModalOpen}

View File

@ -10,6 +10,9 @@ import { Transition, Combobox } from "@headlessui/react";
import projectServices from "services/project.service"; import projectServices from "services/project.service";
// ui // ui
import { AssigneesList, Avatar } from "components/ui"; import { AssigneesList, Avatar } from "components/ui";
// icons
import { UserGroupIcon, MagnifyingGlassIcon, CheckIcon } from "@heroicons/react/24/outline";
// fetch keys // fetch keys
import { PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_MEMBERS } from "constants/fetch-keys";
@ -61,48 +64,83 @@ export const IssueAssigneeSelect: FC<IssueAssigneeSelectProps> = ({
> >
{({ open }: any) => ( {({ open }: any) => (
<> <>
<Combobox.Button className="flex items-center cursor-pointer gap-1 rounded-md"> <Combobox.Button
<div className="flex items-center gap-1 text-xs"> className={({ open }) =>
{value && Array.isArray(value) ? <AssigneesList userIds={value} length={10} /> : null} `flex items-center text-xs cursor-pointer border rounded-md shadow-sm duration-200
</div> ${
open
? "outline-none border-theme bg-theme/5 ring-1 ring-theme "
: "hover:bg-theme/5 "
}`
}
>
{value && value.length > 0 && Array.isArray(value) ? (
<span className="flex items-center justify-center text-xs gap-2 px-3 py-1">
<AssigneesList userIds={value} length={3} showLength={false} />
<span className=" text-gray-500">{value.length} Assignees</span>
</span>
) : (
<span className="flex items-center justify-center text-xs gap-2 px-3 py-1.5">
<UserGroupIcon className="h-4 w-4 text-gray-500 " />
<span className=" text-gray-500">Assignee</span>
</span>
)}
</Combobox.Button> </Combobox.Button>
<Transition <Transition
show={open} show={open}
as={Fragment} as={Fragment}
leave="transition ease-in duration-100" enter="transition ease-out duration-200"
leaveFrom="opacity-100" enterFrom="opacity-0 translate-y-1"
leaveTo="opacity-0" enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
> >
<Combobox.Options <Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`} className={`absolute z-10 max-h-52 min-w-[8rem] mt-1 px-2 py-2 text-xs
rounded-md shadow-md overflow-auto border-none bg-white focus:outline-none`}
> >
<div className="flex justify-start items-center rounded-sm border-[0.6px] bg-gray-100 border-gray-200 w-full px-2">
<MagnifyingGlassIcon className="h-3 w-3 text-gray-500" />
<Combobox.Input <Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none" className="w-full bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none"
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
placeholder="Search" placeholder="Search for a person..."
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
/> />
<div className="py-1"> </div>
<div className="py-1.5">
{filteredOptions ? ( {filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((option) => ( filteredOptions.map((option) => (
<Combobox.Option <Combobox.Option
key={option.value} key={option.value}
className={({ active, selected }) => className={({ active }) =>
`${active ? "bg-indigo-50" : ""} ${ `${
selected ? "bg-indigo-50 font-medium" : "" active ? "bg-gray-200" : ""
} flex cursor-pointer select-none items-center gap-2 truncate px-2 py-1 text-gray-900` } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-500`
} }
value={option.value} value={option.value}
> >
{people && ( {({ selected, active }) => (
<> <div className="flex w-full gap-2 justify-between rounded">
<div className="flex justify-start items-center gap-1">
<Avatar <Avatar
user={people?.find((p) => p.member.id === option.value)?.member} user={people?.find((p) => p.member.id === option.value)?.member}
/> />
{option.display} <span>{option.display}</span>
</> </div>
<div
className={`flex justify-center items-center p-1 rounded border border-gray-500 border-opacity-0 group-hover:border-opacity-100
${selected ? "border-opacity-100 " : ""}
${active ? "bg-gray-100" : ""} `}
>
<CheckIcon
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
/>
</div>
</div>
)} )}
</Combobox.Option> </Combobox.Option>
)) ))

View File

@ -0,0 +1,70 @@
import React from "react";
import { Popover, Transition } from "@headlessui/react";
import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline";
// react-datepicker
import DatePicker from "react-datepicker";
// import "react-datepicker/dist/react-datepicker.css";
import { renderDateFormat } from "helpers/date-time.helper";
type Props = {
value: string | null;
onChange: (val: string | null) => void;
};
export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
<Popover className="flex justify-center items-center relative rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={({ open }) =>
`flex items-center text-xs cursor-pointer border rounded-md shadow-sm duration-200
${
open
? "outline-none border-theme bg-theme/5 ring-1 ring-theme "
: "hover:bg-theme/5 "
}`
}
>
<span className="flex items-center justify-center text-xs gap-2 px-3 py-1.5">
{value ? (
<>
<span className="text-gray-600">{value}</span>
<button onClick={() => onChange(null)}>
<XMarkIcon className="h-3 w-3 text-gray-600" />
</button>
</>
) : (
<>
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0 " />
<span className="text-gray-500">Due Date</span>
</>
)}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 -left-10 z-20 transform overflow-hidden">
<DatePicker
selected={value ? new Date(value) : null}
onChange={(val) => {
if (!val) onChange("");
else onChange(renderDateFormat(val));
}}
dateFormat="dd-MM-yyyy"
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);

View File

@ -4,3 +4,4 @@ export * from "./parent";
export * from "./priority"; export * from "./priority";
export * from "./project"; export * from "./project";
export * from "./state"; export * from "./state";
export * from "./date";

View File

@ -7,13 +7,20 @@ import useSWR from "swr";
// headless ui // headless ui
import { Combobox, Transition } from "@headlessui/react"; import { Combobox, Transition } from "@headlessui/react";
// icons // icons
import { PlusIcon, RectangleGroupIcon, TagIcon } from "@heroicons/react/24/outline"; import {
CheckIcon,
MagnifyingGlassIcon,
PlusIcon,
RectangleGroupIcon,
TagIcon,
} from "@heroicons/react/24/outline";
// services // services
import issuesServices from "services/issues.service"; import issuesServices from "services/issues.service";
// types // types
import type { IIssueLabels } from "types"; import type { IIssueLabels } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { IssueLabelsList } from "components/ui";
type Props = { type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -52,36 +59,57 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
> >
{({ open }: any) => ( {({ open }: any) => (
<> <>
<Combobox.Label className="sr-only">Labels</Combobox.Label>
<Combobox.Button <Combobox.Button
className={`flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`} className={({ open }) =>
`flex items-center text-xs cursor-pointer border rounded-md shadow-sm duration-200
${
open
? "outline-none border-theme bg-theme/5 ring-1 ring-theme "
: "hover:bg-theme/5 "
}`
}
> >
<TagIcon className="h-3 w-3 text-gray-500" /> {value && value.length > 0 ? (
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}> <span className="flex items-center justify-center text-xs gap-2 px-3 py-1">
{Array.isArray(value) <IssueLabelsList
? value.map((v) => issueLabels?.find((l) => l.id === v)?.name).join(", ") || labels={value.map((v) => issueLabels?.find((l) => l.id === v)?.color) ?? []}
"Labels" length={3}
: issueLabels?.find((l) => l.id === value)?.name || "Labels"} showLength
/>
<span className=" text-gray-600">{value.length} Labels</span>
</span> </span>
) : (
<span className="flex items-center justify-center text-xs gap-2 px-3 py-1.5">
<TagIcon className="h-3 w-3 text-gray-500" />
<span className=" text-gray-500">Label</span>
</span>
)}
</Combobox.Button> </Combobox.Button>
<Transition <Transition
show={open} show={open}
as={React.Fragment} as={React.Fragment}
leave="transition ease-in duration-100" enter="transition ease-out duration-200"
leaveFrom="opacity-100" enterFrom="opacity-0 translate-y-1"
leaveTo="opacity-0" enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
> >
<Combobox.Options <Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`} className={`absolute z-10 max-h-52 min-w-[8rem] mt-1 px-2 py-2 text-xs
rounded-md shadow-md overflow-auto border-none bg-white focus:outline-none`}
> >
<div className="flex justify-start items-center rounded-sm border-[0.6px] bg-gray-100 border-gray-200 w-full px-2">
<MagnifyingGlassIcon className="h-3 w-3 text-gray-500" />
<Combobox.Input <Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none" className="w-full bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none"
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
placeholder="Search" placeholder="Search for label..."
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
/> />
<div className="py-1"> </div>
<div className="py-1.5">
{issueLabels && filteredOptions ? ( {issueLabels && filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((label) => { filteredOptions.map((label) => {
@ -92,21 +120,36 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
return ( return (
<Combobox.Option <Combobox.Option
key={label.id} key={label.id}
className={({ active, selected }) => className={({ active }) =>
`${active ? "bg-indigo-50" : ""} ${ `${
selected ? "bg-indigo-50 font-medium" : "" active ? "bg-gray-200" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600`
} }
value={label.id} value={label.id}
> >
{({ selected }) => (
<div className="flex w-full gap-2 justify-between rounded">
<div className="flex justify-start items-center gap-2">
<span <span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full" className="h-3 w-3 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: backgroundColor:
label.color && label.color !== "" ? label.color : "#000", label.color && label.color !== ""
? label.color
: "#000",
}} }}
/> />
{label.name} <span>{label.name}</span>
</div>
<div className="flex justify-center items-center p-1 rounded">
<CheckIcon
className={`h-3 w-3 ${
selected ? "opacity-100" : "opacity-0"
}`}
/>
</div>
</div>
)}
</Combobox.Option> </Combobox.Option>
); );
} else } else
@ -119,20 +162,33 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
{children.map((child) => ( {children.map((child) => (
<Combobox.Option <Combobox.Option
key={child.id} key={child.id}
className={({ active, selected }) => className={({ active }) =>
`${active ? "bg-indigo-50" : ""} ${ `${
selected ? "bg-indigo-50 font-medium" : "" active ? "bg-gray-200" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600`
} }
value={child.id} value={child.id}
> >
{({ selected }) => (
<div className="flex w-full gap-2 justify-between rounded">
<div className="flex justify-start items-center gap-2">
<span <span
className="h-2 w-2 flex-shrink-0 rounded-full" className="h-3 w-3 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: child?.color ?? "black", backgroundColor: child?.color ?? "black",
}} }}
/> />
{child.name} <span>{child.name}</span>
</div>
<div className="flex justify-center items-center p-1 rounded">
<CheckIcon
className={`h-3 w-3 ${
selected ? "opacity-100" : "opacity-0"
}`}
/>
</div>
</div>
)}
</Combobox.Option> </Combobox.Option>
))} ))}
</div> </div>
@ -147,11 +203,13 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
)} )}
<button <button
type="button" type="button"
className="flex select-none w-full items-center gap-2 p-2 text-gray-400 outline-none hover:bg-indigo-50 hover:text-gray-900" className="flex select-none w-full items-center py-2 px-1 rounded hover:bg-gray-200"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
> >
<PlusIcon className="h-3 w-3 text-gray-400" aria-hidden="true" /> <span className="flex justify-start items-center gap-1">
<span className="text-xs whitespace-nowrap">Create label</span> <PlusIcon className="h-4 w-4 text-gray-600" aria-hidden="true" />
<span className="text-gray-600">Create New Label</span>
</span>
</button> </button>
</div> </div>
</Combobox.Options> </Combobox.Options>

View File

@ -6,6 +6,7 @@ import { Listbox, Transition } from "@headlessui/react";
import { getPriorityIcon } from "components/icons/priority-icon"; import { getPriorityIcon } from "components/icons/priority-icon";
// constants // constants
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
import { CheckIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
value: string | null; value: string | null;
@ -16,34 +17,62 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
<Listbox as="div" className="relative" value={value} onChange={onChange}> <Listbox as="div" className="relative" value={value} onChange={onChange}>
{({ open }) => ( {({ open }) => (
<> <>
<Listbox.Button className="flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"> <Listbox.Button
<span className="text-gray-500 grid place-items-center">{getPriorityIcon(value)}</span> className={({ open }) =>
<div className="flex items-center gap-2 capitalize">{value ?? "Priority"}</div> `flex items-center text-xs cursor-pointer border rounded-md shadow-sm duration-200
${
open ? "outline-none border-theme bg-theme/5 ring-1 ring-theme " : "hover:bg-theme/5"
}`
}
>
<span className="flex items-center justify-center text-xs gap-2 px-3 py-1.5">
<span className="flex items-center">
{getPriorityIcon(value, `${value ? "text-xs" : "text-xs text-gray-500"}`)}
</span>
<span className={`${value ? "text-gray-600" : "text-gray-500"} capitalize`}>
{value ?? "Priority"}
</span>
</span>
</Listbox.Button> </Listbox.Button>
<Transition <Transition
show={open} show={open}
as={React.Fragment} as={React.Fragment}
leave="transition ease-in duration-100" enter="transition ease-out duration-200"
leaveFrom="opacity-100" enterFrom="opacity-0 translate-y-1"
leaveTo="opacity-0" enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
> >
<Listbox.Options className="absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-white shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none"> <Listbox.Options
<div className="py-1"> className={`absolute z-10 max-h-52 min-w-[8rem] px-2 py-2 text-xs
rounded-md shadow-md overflow-auto border-none bg-white focus:outline-none`}
>
<div>
{PRIORITIES.map((priority) => ( {PRIORITIES.map((priority) => (
<Listbox.Option <Listbox.Option
key={priority} key={priority}
className={({ selected, active }) => className={({ active }) =>
`${selected ? "bg-indigo-50 font-medium" : ""} ${ `${
active ? "bg-indigo-50" : "" active ? "bg-gray-200" : ""
} relative cursor-pointer select-none p-2 text-gray-900` } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600`
} }
value={priority} value={priority}
> >
<span className="flex items-center gap-2 capitalize"> {({ selected, active }) => (
{getPriorityIcon(priority)} <div className="flex w-full gap-2 justify-between rounded">
{priority ?? "None"} <div className="flex justify-start items-center gap-2">
</span> <span>{getPriorityIcon(priority)}</span>
<span className="capitalize">{priority ?? "None"}</span>
</div>
<div className="flex justify-center items-center p-1 rounded">
<CheckIcon
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
/>
</div>
</div>
)}
</Listbox.Option> </Listbox.Option>
))} ))}
</div> </div>

View File

@ -7,13 +7,19 @@ import useSWR from "swr";
// services // services
import stateService from "services/state.service"; import stateService from "services/state.service";
// headless ui // headless ui
import { Squares2X2Icon, PlusIcon } from "@heroicons/react/24/outline"; import {
Squares2X2Icon,
PlusIcon,
MagnifyingGlassIcon,
CheckIcon,
} from "@heroicons/react/24/outline";
// icons // icons
import { Combobox, Transition } from "@headlessui/react"; import { Combobox, Transition } from "@headlessui/react";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// fetch keys // fetch keys
import { STATE_LIST } from "constants/fetch-keys"; import { STATE_LIST } from "constants/fetch-keys";
import { getStateGroupIcon } from "components/icons";
type Props = { type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -41,6 +47,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
value: state.id, value: state.id,
display: state.name, display: state.name,
color: state.color, color: state.color,
group: state.group,
})); }));
const filteredOptions = const filteredOptions =
@ -48,6 +55,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
? options ? options
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase())); : options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
const currentOption = options?.find((option) => option.value === value);
return ( return (
<Combobox <Combobox
as="div" as="div"
@ -57,64 +65,80 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
> >
{({ open }: any) => ( {({ open }: any) => (
<> <>
<Combobox.Label className="sr-only">State</Combobox.Label>
<Combobox.Button <Combobox.Button
className={`flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`} className={({ open }) =>
`flex items-center text-xs cursor-pointer border rounded-md shadow-sm duration-200
${
open ? "outline-none border-theme bg-theme/5 ring-1 ring-theme " : "hover:bg-theme/5"
}`
}
> >
<Squares2X2Icon className="h-3 w-3 text-gray-500" />
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}>
{value && value !== "" ? ( {value && value !== "" ? (
<span <span className="flex items-center justify-center text-xs gap-2 px-3 py-1.5">
className="h-1.5 w-1.5 flex-shrink-0 rounded-full" {currentOption && currentOption.group
style={{ ? getStateGroupIcon(currentOption.group, "16", "16", currentOption.color)
backgroundColor: options?.find((option) => option.value === value)?.color, : ""}
}} <span className=" text-gray-600">{currentOption?.display}</span>
/>
) : null}
{options?.find((option) => option.value === value)?.display || "State"}
</span> </span>
) : (
<span className="flex items-center justify-center text-xs gap-2 px-3 py-1.5">
<Squares2X2Icon className="h-4 w-4 text-gray-500 " />
<span className=" text-gray-500">{currentOption?.display || "State"}</span>
</span>
)}
</Combobox.Button> </Combobox.Button>
<Transition <Transition
show={open} show={open}
as={React.Fragment} as={React.Fragment}
leave="transition ease-in duration-100" enter="transition ease-out duration-200"
leaveFrom="opacity-100" enterFrom="opacity-0 translate-y-1"
leaveTo="opacity-0" enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
> >
<Combobox.Options <Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`} className={`absolute z-10 max-h-52 min-w-[8rem] mt-1 px-2 py-2 text-xs
rounded-md shadow-md overflow-auto border-none bg-white focus:outline-none`}
> >
<div className="flex justify-start items-center rounded-sm border-[0.6px] bg-gray-100 border-gray-200 w-full px-2">
<MagnifyingGlassIcon className="h-3 w-3 text-gray-500" />
<Combobox.Input <Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none" className="w-full bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none"
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
placeholder="Search" placeholder="Search States"
displayValue={(assigned: any) => assigned?.name} displayValue={(assigned: any) => assigned?.name}
/> />
<div className="py-1"> </div>
<div className="py-1.5">
{filteredOptions ? ( {filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((option) => ( filteredOptions.map((option) => (
<Combobox.Option <Combobox.Option
key={option.value} key={option.value}
className={({ active, selected }) => className={({ active }) =>
`${active ? "bg-indigo-50" : ""} ${ `${
selected ? "bg-indigo-50 font-medium" : "" active ? "bg-gray-200" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600`
} }
value={option.value} value={option.value}
> >
{states && ( {({ selected, active }) =>
<> states && (
<span <div className="flex w-full gap-2 justify-between rounded">
className="h-1.5 w-1.5 flex-shrink-0 rounded-full" <div className="flex justify-start items-center gap-2">
style={{ {getStateGroupIcon(option.group, "16", "16", option.color)}
backgroundColor: option.color, <span>{option.display}</span>
}} </div>
<div className="flex justify-center items-center p-1 rounded">
<CheckIcon
className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`}
/> />
{option.display} </div>
</> </div>
)} )
}
</Combobox.Option> </Combobox.Option>
)) ))
) : ( ) : (
@ -125,11 +149,13 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
)} )}
<button <button
type="button" type="button"
className="flex select-none w-full items-center gap-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-gray-900" className="flex select-none w-full items-center py-2 px-1 rounded hover:bg-gray-200"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
> >
<PlusIcon className="h-3 w-3 text-gray-400" aria-hidden="true" /> <span className="flex justify-start items-center gap-1">
<span className="text-xs whitespace-nowrap">Create state</span> <PlusIcon className="h-4 w-4 text-gray-600" aria-hidden="true" />
<span className="text-gray-600">Create New State</span>
</span>
</button> </button>
</div> </div>
</Combobox.Options> </Combobox.Options>

View File

@ -23,24 +23,17 @@ export const ViewPrioritySelect: React.FC<Props> = ({
isNotAllowed, isNotAllowed,
}) => ( }) => (
<CustomSelect <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} value={issue.state}
onChange={(data: string) => { onChange={(data: string) => {
partialUpdateIssue({ priority: data }); partialUpdateIssue({ priority: data });
}} }}
maxHeight="md" maxHeight="md"
buttonClassName={`flex ${ customButton={
<button
type="button"
className={`grid place-items-center rounded w-6 h-6 ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" 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 ${ } items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent" issue.priority === "urgent"
? "bg-red-100 text-red-600 hover:bg-red-100" ? "bg-red-100 text-red-600 hover:bg-red-100"
: issue.priority === "high" : issue.priority === "high"
@ -51,6 +44,17 @@ export const ViewPrioritySelect: React.FC<Props> = ({
? "bg-green-100 text-green-500 hover:bg-green-100" ? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-gray-100" : "bg-gray-100"
} border-none`} } border-none`}
>
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</span>
</Tooltip>
</button>
}
noChevron noChevron
disabled={isNotAllowed} disabled={isNotAllowed}
selfPositioned={selfPositioned} selfPositioned={selfPositioned}

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
@ -7,14 +8,19 @@ import useSWR, { mutate } from "swr";
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
import projectServices from "services/project.service"; import projectServices from "services/project.service";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { PrimaryButton } from "components/ui/button/primary-button";
import { Button, Input, TextArea, CustomSelect } from "components/ui"; import { Button, Input, TextArea, CustomSelect } from "components/ui";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
// components // components
import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker"; import EmojiIconPicker from "components/emoji-icon-picker";
// helpers // helpers
import { getRandomEmoji } from "helpers/common.helper"; import { getRandomEmoji } from "helpers/common.helper";
@ -36,6 +42,7 @@ const defaultValues: Partial<IProject> = {
description: "", description: "",
network: 2, network: 2,
icon: getRandomEmoji(), icon: getRandomEmoji(),
cover_image: null,
}; };
const IsGuestCondition: React.FC<{ const IsGuestCondition: React.FC<{
@ -172,46 +179,60 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl sm:p-6"> <Dialog.Panel className="transform rounded-lg bg-white text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)}> <div className="relative h-36 w-full rounded-t-lg bg-gray-300">
<div className="space-y-5"> {watch("cover_image") !== null && (
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> <Image
Create Project src={watch("cover_image")!}
</Dialog.Title> layout="fill"
<div className="mt-2"> alt="cover image"
<p className="text-sm text-gray-500"> objectFit="cover"
Create a new project to start working on it. className="rounded-t-lg"
</p>
</div>
<div className="space-y-3">
<div className="flex gap-3">
<div className="flex-shrink-0">
<label htmlFor="icon" className="mb-2 text-gray-500">
Icon
</label>
<Controller
control={control}
name="icon"
render={({ field: { value, onChange } }) => (
<EmojiIconPicker
label={
value ? String.fromCodePoint(parseInt(value)) : "Select Icon"
}
value={value}
onChange={onChange}
/> />
)} )}
<div className="absolute right-2 top-2 p-2">
<button type="button" onClick={handleClose}>
<XMarkIcon className="h-5 w-5 text-white" />
</button>
</div>
<div className="absolute bottom-0 left-0 flex w-full justify-between px-6 py-5">
<div className="absolute left-0 bottom-0 h-16 w-full bg-gradient-to-t from-black opacity-60" />
<h3 className="z-[1] text-xl text-white">Create Project</h3>
<div>
<ImagePickerPopover
label="Change Cover"
onChange={(image) => {
setValue("cover_image", image);
}}
value={watch("cover_image")}
/> />
</div> </div>
<div className="w-full"> </div>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mt-5 space-y-4 px-4 py-3">
<div className="flex items-center gap-x-2">
<div>
<EmojiIconPicker
label={String.fromCodePoint(parseInt(watch("icon")))}
onChange={(emoji) => {
setValue("icon", emoji);
}}
value={watch("icon")}
/>
</div>
<div className="flex-shrink-0 flex-grow">
<Input <Input
id="name" id="name"
label="Name"
name="name" name="name"
type="name" type="name"
placeholder="Enter name" placeholder="Enter name"
error={errors.name} error={errors.name}
register={register} register={register}
className="text-xl"
mode="transparent"
validations={{ validations={{
required: "Name is required", required: "Name is required",
maxLength: { maxLength: {
@ -222,49 +243,14 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
/> />
</div> </div>
</div> </div>
<div>
<h6 className="text-gray-500">Network</h6>
<Controller
name="network"
control={control}
render={({ field: { onChange, value } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
Object.keys(NETWORK_CHOICES).find((k) => k === value.toString())
? NETWORK_CHOICES[
value.toString() as keyof typeof NETWORK_CHOICES
]
: "Select network"
}
input
>
{Object.keys(NETWORK_CHOICES).map((key) => (
<CustomSelect.Option key={key} value={key}>
{NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES]}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div>
<TextArea
id="description"
name="description"
label="Description"
placeholder="Enter description"
error={errors.description}
register={register}
/>
</div>
<div> <div>
<Input <Input
id="identifier" id="identifier"
label="Identifier"
name="identifier" name="identifier"
type="text" type="text"
mode="transparent"
className="text-sm"
placeholder="Enter Project Identifier" placeholder="Enter Project Identifier"
error={errors.identifier} error={errors.identifier}
register={register} register={register}
@ -284,15 +270,52 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
}} }}
/> />
</div> </div>
<div>
<TextArea
id="description"
name="description"
mode="transparent"
className="text-sm"
placeholder="Enter description"
error={errors.description}
register={register}
/>
</div>
<div className="w-40">
<Controller
name="network"
control={control}
render={({ field: { onChange, value } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
Object.keys(NETWORK_CHOICES).find((k) => k === value.toString())
? NETWORK_CHOICES[value.toString() as keyof typeof NETWORK_CHOICES]
: "Select network"
}
input
>
{Object.keys(NETWORK_CHOICES).map((key) => (
<CustomSelect.Option key={key} value={key}>
{NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES]}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div> </div>
</div> </div>
<div className="mt-5 flex justify-end gap-2">
<div className="mt-5 flex justify-end gap-2 border-t-2 px-4 py-3">
<Button theme="secondary" onClick={handleClose}> <Button theme="secondary" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={isSubmitting}> <PrimaryButton type="submit" size="sm" loading={isSubmitting}>
{isSubmitting ? "Creating Project..." : "Create Project"} {isSubmitting ? "Adding project..." : "Add Project"}
</Button> </PrimaryButton>
</div> </div>
</form> </form>
</Dialog.Panel> </Dialog.Panel>

View File

@ -5,14 +5,8 @@ import { Disclosure, Transition } from "@headlessui/react";
import useSWR from "swr"; import useSWR from "swr";
// icons // icons
import { import { ContrastIcon, LayerDiagonalIcon, PeopleGroupIcon } from "components/icons";
ChevronDownIcon, import { ChevronDownIcon, PlusIcon, Cog6ToothIcon } from "@heroicons/react/24/outline";
PlusIcon,
Cog6ToothIcon,
RectangleStackIcon,
RectangleGroupIcon,
} from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useTheme from "hooks/use-theme"; import useTheme from "hooks/use-theme";
@ -31,17 +25,17 @@ const navigation = (workspaceSlug: string, projectId: string) => [
{ {
name: "Issues", name: "Issues",
href: `/${workspaceSlug}/projects/${projectId}/issues`, href: `/${workspaceSlug}/projects/${projectId}/issues`,
icon: RectangleStackIcon, icon: LayerDiagonalIcon,
}, },
{ {
name: "Cycles", name: "Cycles",
href: `/${workspaceSlug}/projects/${projectId}/cycles`, href: `/${workspaceSlug}/projects/${projectId}/cycles`,
icon: CyclesIcon, icon: ContrastIcon,
}, },
{ {
name: "Modules", name: "Modules",
href: `/${workspaceSlug}/projects/${projectId}/modules`, href: `/${workspaceSlug}/projects/${projectId}/modules`,
icon: RectangleGroupIcon, icon: PeopleGroupIcon,
}, },
{ {
name: "Settings", name: "Settings",
@ -81,11 +75,8 @@ export const ProjectSidebarList: FC = () => {
return ( return (
<> <>
<CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} /> <CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} />
<div <div className="no-scrollbar mt-3 flex h-full flex-col space-y-2 overflow-y-auto bg-white border-t border-gray-200 px-6 pt-5 pb-3">
className={`no-scrollbar mt-3 flex h-full flex-col space-y-2 overflow-y-auto bg-primary px-2 pt-5 pb-3 ${ {!sidebarCollapse && <h5 className="text-sm font-semibold text-gray-400">Projects</h5>}
sidebarCollapse ? "rounded-xl" : "rounded-t-3xl"
}`}
>
{projects ? ( {projects ? (
<> <>
{projects.length > 0 ? ( {projects.length > 0 ? (
@ -93,12 +84,13 @@ export const ProjectSidebarList: FC = () => {
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}> <Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
{({ open }) => ( {({ open }) => (
<> <>
<div className="flex items-center">
<Disclosure.Button <Disclosure.Button
className={`flex w-full items-center gap-2 rounded-md p-2 text-left text-sm font-medium ${ as="div"
sidebarCollapse ? "justify-center" : "" className={`flex w-full items-center gap-2 select-none rounded-md py-2 text-left text-sm font-medium ${
sidebarCollapse ? "justify-center" : "justify-between"
}`} }`}
> >
<div className="flex gap-x-2 items-center">
{project.icon ? ( {project.icon ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> <span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{String.fromCodePoint(parseInt(project.icon))} {String.fromCodePoint(parseInt(project.icon))}
@ -110,18 +102,13 @@ export const ProjectSidebarList: FC = () => {
)} )}
{!sidebarCollapse && ( {!sidebarCollapse && (
<span className="flex w-full items-center justify-between"> <p className="w-[125px] text-ellipsis text-[0.875rem] overflow-hidden">
<span className="w-[125px] text-ellipsis overflow-hidden">
{project?.name} {project?.name}
</span> </p>
<span>
<ChevronDownIcon
className={`h-4 w-4 duration-300 ${open ? "rotate-180" : ""}`}
/>
</span>
</span>
)} )}
</Disclosure.Button> </div>
<div className="flex gap-x-1 items-center">
{!sidebarCollapse && ( {!sidebarCollapse && (
<CustomMenu ellipsis> <CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => handleCopyText(project.id)}> <CustomMenu.MenuItem onClick={() => handleCopyText(project.id)}>
@ -129,7 +116,16 @@ export const ProjectSidebarList: FC = () => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
)} )}
{!sidebarCollapse && (
<span>
<ChevronDownIcon
className={`h-4 w-4 duration-300 ${open ? "rotate-180" : ""}`}
/>
</span>
)}
</div> </div>
</Disclosure.Button>
<Transition <Transition
enter="transition duration-100 ease-out" enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0" enterFrom="transform scale-95 opacity-0"
@ -144,8 +140,6 @@ export const ProjectSidebarList: FC = () => {
} flex flex-col gap-y-1`} } flex flex-col gap-y-1`}
> >
{navigation(workspaceSlug as string, project?.id).map((item) => { {navigation(workspaceSlug as string, project?.id).map((item) => {
const hi = "hi";
if (item.name === "Cycles" && !project.cycle_view) return; if (item.name === "Cycles" && !project.cycle_view) return;
if (item.name === "Modules" && !project.module_view) return; if (item.name === "Modules" && !project.module_view) return;
@ -154,18 +148,20 @@ export const ProjectSidebarList: FC = () => {
<a <a
className={`group flex items-center rounded-md px-2 py-2 text-xs font-medium outline-none ${ className={`group flex items-center rounded-md px-2 py-2 text-xs font-medium outline-none ${
item.href === router.asPath item.href === router.asPath
? "bg-gray-200 text-gray-900" ? "bg-indigo-50 text-gray-900"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900" : "text-gray-500 hover:bg-indigo-50 hover:text-gray-900 focus:bg-indigo-50 focus:text-gray-900"
} ${sidebarCollapse ? "justify-center" : ""}`} } ${sidebarCollapse ? "justify-center" : ""}`}
> >
<div className="grid place-items-center">
<item.icon <item.icon
className={`h-4 w-4 flex-shrink-0 ${ className={`w-5 h-5 flex-shrink-0 ${
item.href === router.asPath item.href === router.asPath
? "text-gray-900" ? "text-gray-900"
: "text-gray-500 group-hover:text-gray-900" : "text-gray-500 group-hover:text-gray-900"
} ${!sidebarCollapse ? "mr-3" : ""}`} } ${!sidebarCollapse ? "mr-3" : ""}`}
aria-hidden="true" aria-hidden="true"
/> />
</div>
{!sidebarCollapse && item.name} {!sidebarCollapse && item.name}
</a> </a>
</Link> </Link>

View File

@ -18,7 +18,7 @@ type AvatarProps = {
}; };
export const Avatar: React.FC<AvatarProps> = ({ user, index }) => ( export const Avatar: React.FC<AvatarProps> = ({ user, index }) => (
<div className={`relative h-5 w-5 rounded-full ${index && index !== 0 ? "-ml-2.5" : ""}`}> <div className={`relative h-5 w-5 rounded-full ${index && index !== 0 ? "-ml-3.5" : ""}`}>
{user && user.avatar && user.avatar !== "" ? ( {user && user.avatar && user.avatar !== "" ? (
<div <div
className={`h-5 w-5 rounded-full border-2 ${ className={`h-5 w-5 rounded-full border-2 ${
@ -47,9 +47,15 @@ type AsigneesListProps = {
users?: Partial<IUser[]> | (Partial<IUserLite> | undefined)[] | Partial<IUserLite>[]; users?: Partial<IUser[]> | (Partial<IUserLite> | undefined)[] | Partial<IUserLite>[];
userIds?: string[]; userIds?: string[];
length?: number; length?: number;
showLength?: boolean;
}; };
export const AssigneesList: React.FC<AsigneesListProps> = ({ users, userIds, length = 5 }) => { export const AssigneesList: React.FC<AsigneesListProps> = ({
users,
userIds,
length = 5,
showLength = true,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -82,7 +88,13 @@ export const AssigneesList: React.FC<AsigneesListProps> = ({ users, userIds, len
return <Avatar key={userId} user={user} index={index} />; return <Avatar key={userId} user={user} index={index} />;
})} })}
{userIds.length > length ? <span>+{userIds.length - length}</span> : null} {showLength ? (
userIds.length > length ? (
<span>+{userIds.length - length}</span>
) : null
) : (
""
)}
</> </>
)} )}
</> </>

View File

@ -0,0 +1 @@
export * from "./primary-button";

View File

@ -0,0 +1,32 @@
type TButtonProps = {
children: React.ReactNode;
className?: string;
onClick?: () => void;
type?: "button" | "submit" | "reset";
disabled?: boolean;
loading?: boolean;
size?: "sm" | "md" | "lg";
};
export const PrimaryButton: React.FC<TButtonProps> = (props) => {
const { children, className, onClick, type, disabled, loading, size = "md" } = props;
return (
<button
type={type}
className={`hover:bg-opacity-90 transition-colors text-white rounded-lg ${
size === "sm"
? "px-2.5 py-1.5 text-sm"
: size === "md"
? "px-3 py-2 text-base"
: "px-4 py-3 text-lg"
} ${disabled ? "bg-gray-400 cursor-not-allowed" : "bg-theme"} ${className || ""} ${
loading ? "cursor-wait" : ""
}`}
onClick={onClick}
disabled={disabled || loading}
>
<div className="flex items-center">{children}</div>
</button>
);
};

View File

@ -16,6 +16,7 @@ type Props = {
textAlignment?: "left" | "center" | "right"; textAlignment?: "left" | "center" | "right";
noBorder?: boolean; noBorder?: boolean;
optionsPosition?: "left" | "right"; optionsPosition?: "left" | "right";
customButton?: JSX.Element;
}; };
type MenuItemProps = { type MenuItemProps = {
@ -36,6 +37,7 @@ const CustomMenu = ({
textAlignment, textAlignment,
noBorder = false, noBorder = false,
optionsPosition = "right", optionsPosition = "right",
customButton,
}: 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>

View File

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

View File

@ -15,3 +15,4 @@ export * from "./progress-bar";
export * from "./select"; export * from "./select";
export * from "./spinner"; export * from "./spinner";
export * from "./tooltip"; export * from "./tooltip";
export * from "./labels-list";

View File

@ -0,0 +1,32 @@
import React from "react";
type IssueLabelsListProps = {
labels?: (string | undefined)[];
length?: number;
showLength?: boolean;
};
export const IssueLabelsList: React.FC<IssueLabelsListProps> = ({
labels,
length = 5,
showLength = true,
}) => (
<>
{labels && (
<>
{labels.map((color, index) => (
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}>
<span
className={`h-4 w-4 flex-shrink-0 rounded-full border border-white
`}
style={{
backgroundColor: color,
}}
/>
</div>
))}
{labels.length > length ? <span>+{labels.length - length}</span> : null}
</>
)}
</>
);

View File

@ -56,30 +56,10 @@ export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
return ( return (
<div <div
className={`flex w-full items-center justify-between self-baseline bg-primary px-2 py-2 ${ className={`flex w-full items-center justify-between self-baseline bg-white border-t border-gray-200 px-6 py-2 ${
sidebarCollapse ? "flex-col-reverse" : "" sidebarCollapse ? "flex-col-reverse" : ""
}`} }`}
> >
<button
type="button"
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:flex ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => toggleCollapsed()}
>
<ArrowLongLeftIcon
className={`h-4 w-4 flex-shrink-0 text-gray-500 duration-300 group-hover:text-gray-900 ${
sidebarCollapse ? "rotate-180" : ""
}`}
/>
</button>
<button
type="button"
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:hidden"
onClick={() => setSidebarActive(false)}
>
<ArrowLongLeftIcon className="h-4 w-4 flex-shrink-0 text-gray-500 group-hover:text-gray-900" />
</button>
<button <button
type="button" type="button"
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${ className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${
@ -93,9 +73,47 @@ export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
}} }}
title="Shortcuts" title="Shortcuts"
> >
<BoltIcon className="h-4 w-4 text-gray-500" /> <BoltIcon className={`text-gray-500 ${sidebarCollapse ? "h-4 w-4" : "h-6 w-6"}`} />
{!sidebarCollapse && <span>Shortcuts</span>} {!sidebarCollapse && <span>Shortcuts</span>}
</button> </button>
<button
type="button"
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
title="Help"
>
<QuestionMarkCircleIcon
className={`text-gray-500 ${sidebarCollapse ? "h-4 w-4" : "h-6 w-6"}`}
/>
{!sidebarCollapse && <span>Help</span>}
</button>
<button
type="button"
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:hidden"
onClick={() => setSidebarActive(false)}
>
<ArrowLongLeftIcon
className={`flex-shrink-0 text-gray-500 group-hover:text-gray-900 ${
sidebarCollapse ? "h-4 w-4" : "h-6 w-6"
}`}
/>
</button>
<button
type="button"
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 md:flex ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => toggleCollapsed()}
>
<ArrowLongLeftIcon
className={`flex-shrink-0 text-gray-500 duration-300 group-hover:text-gray-900 ${
sidebarCollapse ? "h-4 w-4 rotate-180" : "h-6 w-6"
}`}
/>
</button>
<div className="relative"> <div className="relative">
<Transition <Transition
show={isNeedHelpOpen} show={isNeedHelpOpen}
@ -123,17 +141,6 @@ export const WorkspaceHelpSection: FC<WorkspaceHelpSectionProps> = (props) => {
))} ))}
</div> </div>
</Transition> </Transition>
<button
type="button"
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-gray-500 outline-none hover:bg-gray-100 hover:text-gray-900 ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
title="Help"
>
<QuestionMarkCircleIcon className="h-4 w-4 text-gray-500" />
{!sidebarCollapse && <span>Help?</span>}
</button>
</div> </div>
</div> </div>
); );

View File

@ -69,7 +69,6 @@ export const WorkspaceSidebarDropdown = () => {
return ( return (
<div className="relative"> <div className="relative">
<Menu as="div" className="col-span-4 inline-block w-full text-left"> <Menu as="div" className="col-span-4 inline-block w-full text-left">
<div className="w-full">
<Menu.Button <Menu.Button
className={`inline-flex w-full items-center justify-between rounded-md px-1 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${ className={`inline-flex w-full items-center justify-between rounded-md px-1 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${
!sidebarCollapse !sidebarCollapse
@ -77,7 +76,7 @@ export const WorkspaceSidebarDropdown = () => {
: "" : ""
}`} }`}
> >
<div className="flex items-center gap-x-1"> <div className="flex items-center mx-auto gap-x-1">
<div className="relative flex h-5 w-5 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white"> <div className="relative flex h-5 w-5 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? ( {activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
<Image <Image
@ -107,7 +106,6 @@ export const WorkspaceSidebarDropdown = () => {
</div> </div>
)} )}
</Menu.Button> </Menu.Button>
</div>
<Transition <Transition
as={Fragment} as={Fragment}

View File

@ -2,39 +2,34 @@ import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// icons // icons
import { import { GridViewIcon, AssignmentClipboardIcon, TickMarkIcon, SettingIcon } from "components/icons";
ClipboardDocumentListIcon,
Cog6ToothIcon,
HomeIcon,
RectangleStackIcon,
} from "@heroicons/react/24/outline";
// hooks // hooks
import useTheme from "hooks/use-theme"; import useTheme from "hooks/use-theme";
const workspaceLinks = (workspaceSlug: string) => [ const workspaceLinks = (workspaceSlug: string) => [
{ {
icon: HomeIcon, icon: GridViewIcon,
name: "Home", name: "Dashboard",
href: `/${workspaceSlug}`, href: `/${workspaceSlug}`,
}, },
{ {
icon: ClipboardDocumentListIcon, icon: AssignmentClipboardIcon,
name: "Projects", name: "Projects",
href: `/${workspaceSlug}/projects`, href: `/${workspaceSlug}/projects`,
}, },
{ {
icon: RectangleStackIcon, icon: TickMarkIcon,
name: "My Issues", name: "My Issues",
href: `/${workspaceSlug}/me/my-issues`, href: `/${workspaceSlug}/me/my-issues`,
}, },
{ {
icon: Cog6ToothIcon, icon: SettingIcon,
name: "Settings", name: "Settings",
href: `/${workspaceSlug}/settings`, href: `/${workspaceSlug}/settings`,
}, },
]; ];
export const WorkspaceSidebarMenu = () => { export const WorkspaceSidebarMenu: React.FC = () => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -49,15 +44,15 @@ export const WorkspaceSidebarMenu = () => {
<a <a
className={`${ className={`${
link.href === router.asPath link.href === router.asPath
? "bg-gray-200 text-gray-900" ? "bg-indigo-50 text-gray-900"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100" : "text-gray-500 hover:bg-indigo-50 hover:text-gray-900 focus:bg-indigo-50"
} group flex items-center gap-3 rounded-md p-2 text-xs font-medium outline-none ${ } group flex items-center gap-3 rounded-md p-2 text-xs font-medium outline-none ${
sidebarCollapse ? "justify-center" : "" sidebarCollapse ? "justify-center" : ""
}`} }`}
> >
<link.icon <link.icon
className={`${ className={`${
link.href === router.asPath ? "text-gray-900" : "text-gray-500" link.href === router.asPath ? "text-gray-900" : "text-gray-600"
} h-4 w-4 flex-shrink-0 group-hover:text-gray-900`} } h-4 w-4 flex-shrink-0 group-hover:text-gray-900`}
aria-hidden="true" aria-hidden="true"
/> />

View File

@ -1,5 +0,0 @@
export const CYCLE_STATUS = [
{ label: "Started", value: "started", color: "#5e6ad2" },
{ label: "Completed", value: "completed", color: "#eb5757" },
{ label: "Draft", value: "draft", color: "#f2c94c" },
];

View File

@ -35,6 +35,9 @@ export const PROJECT_GITHUB_REPOSITORY = (projectId: string) =>
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`; export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`; export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;
export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAIL_${cycleId}`; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAIL_${cycleId}`;
export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) => `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`;
export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId}`;
export const CYCLE_COMPLETE_LIST = (projectId: string) => `CYCLE_COMPLETE_LIST_${projectId}`;
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`; export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
export const STATE_DETAIL = "STATE_DETAIL"; export const STATE_DETAIL = "STATE_DETAIL";

View File

@ -88,3 +88,17 @@ export const timeAgo = (time: any) => {
} }
return time; return time;
}; };
export const getDateRangeStatus = (startDate: string , endDate: string ) => {
const now = new Date();
const start = new Date(startDate);
const end = new Date(endDate);
if (end < now) {
return "completed";
} else if (start <= now && end >= now) {
return "current";
} else {
return "upcoming";
}
}

View File

@ -11,7 +11,7 @@ type Props = {
}; };
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => ( const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => (
<div className="flex w-full flex-row items-center justify-between gap-y-4 border-b border-gray-200 bg-gray-50 px-5 py-4 "> <div className="flex w-full flex-row items-center justify-between gap-y-4 border-b border-gray-200 bg-white px-5 py-4 ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="block md:hidden"> <div className="block md:hidden">
<Button <Button

View File

@ -20,7 +20,7 @@ const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) =>
return ( return (
<nav className="relative z-20 h-screen"> <nav className="relative z-20 h-screen">
<div <div
className={`${sidebarCollapse ? "" : "w-auto md:w-60"} fixed inset-y-0 top-0 ${ className={`${sidebarCollapse ? "" : "w-auto md:w-80"} fixed inset-y-0 top-0 ${
toggleSidebar ? "left-0" : "-left-60 md:left-0" toggleSidebar ? "left-0" : "-left-60 md:left-0"
} flex h-full flex-col bg-white duration-300 md:relative`} } flex h-full flex-col bg-white duration-300 md:relative`}
> >

View File

@ -205,7 +205,7 @@ const AppLayout: FC<AppLayoutProps> = ({
) : isMember ? ( ) : isMember ? (
<div <div
className={`w-full flex-grow ${ className={`w-full flex-grow ${
noPadding ? "" : settingsLayout ? "p-5 pb-5 lg:px-16 lg:pt-10" : "p-5" noPadding ? "" : settingsLayout ? "p-9 lg:px-16 lg:pt-10" : "p-9"
} ${ } ${
bg === "primary" bg === "primary"
? "bg-primary" ? "bg-primary"

View File

@ -25,6 +25,7 @@ import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers // helpers
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { getDateRangeStatus } from "helpers/date-time.helper";
// types // types
import { CycleIssueResponse, UserAuth } from "types"; import { CycleIssueResponse, UserAuth } from "types";
// fetch-keys // fetch-keys
@ -78,6 +79,11 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
: null : null
); );
const cycleStatus =
cycleDetails?.start_date && cycleDetails?.end_date
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
: "draft";
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>( const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null, workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
workspaceSlug && projectId && cycleId workspaceSlug && projectId && cycleId
@ -218,6 +224,7 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
</div> </div>
)} )}
<CycleDetailsSidebar <CycleDetailsSidebar
cycleStatus={cycleStatus}
issues={cycleIssuesArray ?? []} issues={cycleIssuesArray ?? []}
cycle={cycleDetails} cycle={cycleDetails}
isOpen={cycleSidebar} isOpen={cycleSidebar}

View File

@ -1,30 +1,47 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
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";
import { CyclesIcon } from "components/icons";
// services // services
import cycleService from "services/cycles.service"; import cycleService from "services/cycles.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
// layouts // layouts
import AppLayout from "layouts/app-layout"; import AppLayout from "layouts/app-layout";
// components // components
import { CreateUpdateCycleModal, CyclesListView } from "components/cycles"; import { CompletedCyclesListProps, CreateUpdateCycleModal, CyclesList } from "components/cycles";
// ui // ui
import { HeaderButton, EmptySpace, EmptySpaceItem, Loader } from "components/ui"; import { HeaderButton, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
// types // types
import { ICycle, SelectCycleType } from "types"; import { SelectCycleType } from "types";
import type { NextPage, GetServerSidePropsContext } from "next"; import type { NextPage, GetServerSidePropsContext } from "next";
// fetching keys // fetching keys
import { CYCLE_LIST, PROJECT_DETAILS, WORKSPACE_DETAILS } from "constants/fetch-keys"; import {
CYCLE_CURRENT_AND_UPCOMING_LIST,
CYCLE_DRAFT_LIST,
PROJECT_DETAILS,
} from "constants/fetch-keys";
const CompletedCyclesList = dynamic<CompletedCyclesListProps>(
() => import("components/cycles").then((a) => a.CompletedCyclesList),
{
ssr: false,
loading: () => (
<Loader className="mb-5">
<Loader.Item height="12rem" width="100%" />
</Loader>
),
}
);
const ProjectCycles: NextPage = () => { const ProjectCycles: NextPage = () => {
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>(); const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
@ -34,43 +51,25 @@ const ProjectCycles: NextPage = () => {
query: { workspaceSlug, projectId }, query: { workspaceSlug, projectId },
} = useRouter(); } = useRouter();
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
);
const { data: activeProject } = useSWR( const { data: activeProject } = useSWR(
activeWorkspace && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
activeWorkspace && projectId workspaceSlug && projectId
? () => projectService.getProject(activeWorkspace.slug, projectId as string) ? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null : null
); );
const { data: cycles } = useSWR<ICycle[]>( const { data: draftCycles } = useSWR(
activeWorkspace && projectId ? CYCLE_LIST(projectId as string) : null, workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null,
activeWorkspace && projectId workspaceSlug && projectId
? () => cycleService.getCycles(activeWorkspace.slug, projectId as string) ? () => cycleService.getDraftCycles(workspaceSlug as string, projectId as string)
: null : null
); );
const getCycleStatus = (startDate: string, endDate: string) => { const { data: currentAndUpcomingCycles } = useSWR(
const today = new Date(); workspaceSlug && projectId ? CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string) : null,
workspaceSlug && projectId
if (today < new Date(startDate)) return "upcoming"; ? () => cycleService.getCurrentAndUpcomingCycles(workspaceSlug as string, projectId as string)
else if (today > new Date(endDate)) return "completed"; : null
else return "current";
};
const currentCycles = cycles?.filter(
(c) => getCycleStatus(c.start_date, c.end_date) === "current"
);
const upcomingCycles = cycles?.filter(
(c) => getCycleStatus(c.start_date, c.end_date) === "upcoming"
);
const completedCycles = cycles?.filter(
(c) => getCycleStatus(c.start_date, c.end_date) === "completed"
); );
useEffect(() => { useEffect(() => {
@ -110,13 +109,11 @@ const ProjectCycles: NextPage = () => {
handleClose={() => setCreateUpdateCycleModal(false)} handleClose={() => setCreateUpdateCycleModal(false)}
data={selectedCycle} data={selectedCycle}
/> />
{cycles ? (
cycles.length > 0 ? (
<div className="space-y-8"> <div className="space-y-8">
<h3 className="text-xl font-medium leading-6 text-gray-900">Current Cycle</h3> <h3 className="text-xl font-medium leading-6 text-gray-900">Current Cycle</h3>
<div className="space-y-5"> <div className="space-y-5">
<CyclesListView <CyclesList
cycles={currentCycles ?? []} cycles={currentAndUpcomingCycles?.current_cycle ?? []}
setCreateUpdateCycleModal={setCreateUpdateCycleModal} setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle} setSelectedCycle={setSelectedCycle}
type="current" type="current"
@ -126,76 +123,57 @@ const ProjectCycles: NextPage = () => {
<Tab.Group> <Tab.Group>
<Tab.List <Tab.List
as="div" as="div"
className="grid grid-cols-2 items-center gap-2 rounded-lg bg-gray-100 p-2 text-sm" className="flex justify-between items-center gap-2 rounded-lg bg-gray-100 p-2 text-sm"
> >
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` `w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
} }
> >
Upcoming Upcoming
</Tab> </Tab>
<Tab <Tab
className={({ selected }) => className={({ selected }) =>
`rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` `w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
} }
> >
Completed Completed
</Tab> </Tab>
<Tab
className={({ selected }) =>
` w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
}
>
Draft
</Tab>
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel as="div" className="mt-8 space-y-5"> <Tab.Panel as="div" className="mt-8 space-y-5">
<CyclesListView <CyclesList
cycles={upcomingCycles ?? []} cycles={currentAndUpcomingCycles?.upcoming_cycle ?? []}
setCreateUpdateCycleModal={setCreateUpdateCycleModal} setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle} setSelectedCycle={setSelectedCycle}
type="upcoming" type="upcoming"
/> />
</Tab.Panel> </Tab.Panel>
<Tab.Panel as="div" className="mt-8 space-y-5"> <Tab.Panel as="div" className="mt-8 space-y-5">
<CyclesListView <CompletedCyclesList
cycles={completedCycles ?? []}
setCreateUpdateCycleModal={setCreateUpdateCycleModal} setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle} setSelectedCycle={setSelectedCycle}
type="completed"
/> />
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>
<Tab.Panel as="div" className="mt-8 space-y-5">
<CyclesList
cycles={draftCycles?.draft_cycles ?? []}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
type="upcoming"
/>
</Tab.Panel>
</Tab.Group> </Tab.Group>
</div> </div>
</div> </div>
) : (
<div className="flex h-full w-full flex-col items-center justify-center px-4">
<EmptySpace
title="You don't have any cycle yet."
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
Icon={CyclesIcon}
>
<EmptySpaceItem
title="Create a new cycle"
description={
<span>
Use <pre className="inline rounded bg-gray-200 px-2 py-1">Q</pre> shortcut to
create a new cycle
</span>
}
Icon={PlusIcon}
action={() => {
const e = new KeyboardEvent("keydown", {
key: "q",
});
document.dispatchEvent(e);
}}
/>
</EmptySpace>
</div>
)
) : (
<Loader className="space-y-5">
<Loader.Item height="150px" />
<Loader.Item height="150px" />
</Loader>
)}
</AppLayout> </AppLayout>
); );
}; };

View File

@ -7,15 +7,13 @@ import useSWR, { mutate } from "swr";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// react-dropzone
import Dropzone from "react-dropzone";
// icons // icons
import { LinkIcon } from "@heroicons/react/24/outline"; import { LinkIcon } from "@heroicons/react/24/outline";
// lib // lib
import { requiredWorkspaceAdmin } from "lib/auth"; import { requiredWorkspaceAdmin } from "lib/auth";
// services // services
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
import fileServices from "services/file.service";
// layouts // layouts
import AppLayout from "layouts/app-layout"; import AppLayout from "layouts/app-layout";
// hooks // hooks
@ -52,7 +50,6 @@ type TWorkspaceSettingsProps = {
const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => { const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState<File | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false); const [isImageUploading, setIsImageUploading] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
@ -122,9 +119,11 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
<ImageUploadModal <ImageUploadModal
isOpen={isImageUploadModalOpen} isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)} onClose={() => setIsImageUploadModalOpen(false)}
onSuccess={() => { onSuccess={(imageUrl) => {
setIsImageUploading(true);
setValue("logo", imageUrl);
setIsImageUploadModalOpen(false); setIsImageUploadModalOpen(false);
handleSubmit(onSubmit)(); handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
}} }}
value={watch("logo")} value={watch("logo")}
/> />
@ -147,24 +146,11 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
<div className="col-span lg:col-span-5"> <div className="col-span lg:col-span-5">
<h4 className="text-md mb-1 leading-6 text-gray-900">Logo</h4> <h4 className="text-md mb-1 leading-6 text-gray-900">Logo</h4>
<div className="flex w-full gap-2"> <div className="flex w-full gap-2">
<Dropzone <button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
multiple={false} {watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
accept={{
"image/*": [],
}}
onDrop={(files) => {
setImage(files[0]);
}}
>
{({ getRootProps, getInputProps }) => (
<div>
<input {...getInputProps()} />
<div {...getRootProps()}>
{(watch("logo") && watch("logo") !== null && watch("logo") !== "") ||
(image && image !== null) ? (
<div className="relative mx-auto flex h-12 w-12"> <div className="relative mx-auto flex h-12 w-12">
<Image <Image
src={image ? URL.createObjectURL(image) : watch("logo") ?? ""} src={watch("logo")!}
alt="Workspace Logo" alt="Workspace Logo"
objectFit="cover" objectFit="cover"
layout="fill" layout="fill"
@ -177,32 +163,14 @@ const WorkspaceSettings: NextPage<TWorkspaceSettingsProps> = (props) => {
{activeWorkspace?.name?.charAt(0) ?? "N"} {activeWorkspace?.name?.charAt(0) ?? "N"}
</div> </div>
)} )}
</div> </button>
</div>
)}
</Dropzone>
<div> <div>
<p className="mb-2 text-sm text-gray-500"> <p className="mb-2 text-sm text-gray-500">
Max file size is 5MB. Supported file types are .jpg and .png. Max file size is 5MB. Supported file types are .jpg and .png.
</p> </p>
<Button <Button
onClick={() => { onClick={() => {
if (image === null) return; setIsImageUploadModalOpen(true);
setIsImageUploading(true);
const formData = new FormData();
formData.append("asset", image);
formData.append("attributes", JSON.stringify({}));
fileServices
.uploadFile(workspaceSlug as string, formData)
.then((response) => {
const imageUrl = response.asset;
setValue("logo", imageUrl);
handleSubmit(onSubmit)();
setIsImageUploading(false);
})
.catch(() => {
setIsImageUploading(false);
});
}} }}
> >
{isImageUploading ? "Uploading..." : "Upload"} {isImageUploading ? "Uploading..." : "Upload"}

View File

@ -1,7 +1,7 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
// types // types
import type { ICycle } from "types"; import type { CompletedCyclesResponse, CurrentAndUpcomingCyclesResponse, DraftCyclesResponse, ICycle } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -87,6 +87,47 @@ class ProjectCycleServices extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async cycleDateCheck(workspaceSlug: string, projectId: string, data: {
start_date: string,
end_date: string
}): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise<CurrentAndUpcomingCyclesResponse> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getDraftCycles(workspaceSlug: string, projectId: string): Promise<DraftCyclesResponse> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getCompletedCycles(workspaceSlug: string, projectId: string): Promise<CompletedCyclesResponse> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
} }
export default new ProjectCycleServices(); export default new ProjectCycleServices();

View File

@ -3,6 +3,30 @@ import APIService from "services/api.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;
interface UnSplashImage {
id: string;
created_at: Date;
updated_at: Date;
promoted_at: Date;
width: number;
height: number;
color: string;
blur_hash: string;
description: null;
alt_description: string;
urls: UnSplashImageUrls;
[key: string]: any;
}
interface UnSplashImageUrls {
raw: string;
full: string;
regular: string;
small: string;
thumb: string;
small_s3: string;
}
class FileServices extends APIService { class FileServices extends APIService {
constructor() { constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
@ -15,6 +39,24 @@ class FileServices extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> {
const clientId = process.env.NEXT_PUBLIC_UNSPLASH_ACCESS;
const url = query
? `https://api.unsplash.com/search/photos/?client_id=${clientId}&query=${query}&page=${page}&per_page=20`
: `https://api.unsplash.com/photos/?client_id=${clientId}&page=${page}&per_page=20`;
return this.request({
method: "get",
url,
})
.then((response) => {
return response?.data?.results ?? response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
} }
export default new FileServices(); export default new FileServices();

View File

@ -7,16 +7,32 @@ export interface ICycle {
updated_at: Date; updated_at: Date;
name: string; name: string;
description: string; description: string;
start_date: string; start_date: string | null;
end_date: string; end_date: string | null;
status: string;
created_by: string; created_by: string;
updated_by: string; updated_by: string;
project: string; project: string;
workspace: string; workspace: string;
issue: string; issue: string;
current_cycle: [];
upcoming_cycle: [];
past_cycles: [];
} }
export interface CurrentAndUpcomingCyclesResponse {
current_cycle : ICycle[];
upcoming_cycle : ICycle[];
}
export interface DraftCyclesResponse {
draft_cycles : ICycle[];
}
export interface CompletedCyclesResponse {
completed_cycles : ICycle[];
}
export interface CycleIssueResponse { export interface CycleIssueResponse {
id: string; id: string;
issue_detail: IIssue; issue_detail: IIssue;