Setting design
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-06-17 16:14:18 +03:30
parent 92033fb94c
commit 9c6aa47cf7
12 changed files with 384 additions and 52 deletions
@@ -1,6 +1,7 @@
import { Calendar, Paperclip2, Profile2User, Tag, TickSquare, type Icon } from "iconsax-react";
import { type FC, type ReactNode } from "react";
import { type FC, type ReactNode, useRef } from "react";
import { useTranslation } from "react-i18next";
import TaskDetailPopoverPanel from "./TaskDetailPopoverPanel";
import type { TaskDetailTab } from "./types";
type TabItem = {
@@ -24,49 +25,67 @@ type Props = {
usersPopover?: ReactNode;
checklistPopover?: ReactNode;
attachmentPopover?: ReactNode;
datePopover?: ReactNode;
};
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover }) => {
const POPOVER_TABS = new Set<TaskDetailTab>(["labels", "users", "checklist", "attachment", "date"]);
type PopoverTab = "labels" | "users" | "checklist" | "attachment" | "date";
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover, datePopover }) => {
const { t } = useTranslation("global");
const anchorRefs = useRef<Record<PopoverTab, { current: HTMLElement | null }>>({
labels: { current: null },
users: { current: null },
checklist: { current: null },
attachment: { current: null },
date: { current: null },
});
const popoverByTab: Record<PopoverTab, ReactNode> = {
labels: labelsPopover,
users: usersPopover,
checklist: checklistPopover,
attachment: attachmentPopover,
date: datePopover,
};
return (
<div className="flex flex-wrap items-center gap-2">
{TABS.map(({ id, icon: Icon, labelKey }) => {
const isActive = activeTab === id;
<>
<div className="flex flex-wrap items-center gap-2">
{TABS.map(({ id, icon: Icon, labelKey }) => {
const isActive = activeTab === id;
const hasPopover = POPOVER_TABS.has(id);
const button = (
<button
type="button"
onClick={() => onTabChange(id)}
className={`flex items-center gap-1.5 border rounded-lg px-3 py-2 text-[11px] cursor-pointer transition-colors ${isActive ? "bg-[#4A4A4A] text-white border-transparent" : "bg-white/50 text-[#292D32] border-white"}`}
>
<Icon size={16} color={isActive ? "#FFFFFF" : "#292D32"} />
<span>{t(labelKey)}</span>
</button>
);
if (id === "labels" || id === "users" || id === "checklist" || id === "attachment") {
return (
<div key={id} className="relative">
{button}
{id === "labels"
? labelsPopover
: id === "users"
? usersPopover
: id === "checklist"
? checklistPopover
: attachmentPopover}
<div
key={id}
ref={(element) => {
if (!hasPopover) return;
anchorRefs.current[id as PopoverTab].current = element;
}}
>
<button
type="button"
onClick={() => onTabChange(id)}
className={`flex items-center gap-1.5 border rounded-lg px-3 py-2 text-[11px] cursor-pointer transition-colors ${isActive ? "bg-[#4A4A4A] text-white border-transparent" : "bg-white/50 text-[#292D32] border-white"}`}
>
<Icon size={16} color={isActive ? "#FFFFFF" : "#292D32"} />
<span>{t(labelKey)}</span>
</button>
</div>
);
}
})}
</div>
return (
<div key={id}>
{button}
</div>
);
})}
</div>
{(Object.keys(popoverByTab) as PopoverTab[]).map((tab) =>
popoverByTab[tab] ? (
<TaskDetailPopoverPanel key={tab} anchorRef={anchorRefs.current[tab]}>
{popoverByTab[tab]}
</TaskDetailPopoverPanel>
) : null,
)}
</>
);
};
@@ -0,0 +1,54 @@
import { type FC, type ReactNode, useLayoutEffect, useState } from "react";
import { createPortal } from "react-dom";
import { clx } from "../../../../helpers/utils";
type Props = {
anchorRef: { current: HTMLElement | null };
children: ReactNode;
className?: string;
width?: number;
};
const TaskDetailPopoverPanel: FC<Props> = ({ anchorRef, children, className, width = 300 }) => {
const [position, setPosition] = useState<{ top: number; left: number } | null>(null);
useLayoutEffect(() => {
const updatePosition = () => {
const anchor = anchorRef.current;
if (!anchor) return;
const rect = anchor.getBoundingClientRect();
const padding = 8;
let left = rect.right - width;
left = Math.max(padding, Math.min(left, window.innerWidth - width - padding));
setPosition({
top: rect.bottom + 8,
left,
});
};
updatePosition();
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
return () => {
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
};
}, [anchorRef, width]);
if (!position) return null;
return createPortal(
<div
style={{ top: position.top, left: position.left, width }}
className={clx("fixed z-[80] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80", className)}
>
{children}
</div>,
document.body,
);
};
export default TaskDetailPopoverPanel;
@@ -1,5 +1,7 @@
import { type FC, useState } from "react";
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
import type DateObject from "react-date-object";
import { DEFAULT_CHECKLISTS } from "./checklist/constants";
import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover";
import type { TaskChecklist } from "./checklist/types";
@@ -30,6 +32,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
const isUsersOpen = activeTab === "users";
const isChecklistOpen = activeTab === "checklist";
const isAttachmentOpen = activeTab === "attachment";
const isDateOpen = activeTab === "date";
const handleTabChange = (tab: TaskDetailTab) => {
if (tab === "labels" && activeTab !== "labels") {
@@ -94,6 +97,14 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
handleAttachmentClose();
};
const handleDateClose = () => {
if (isDateOpen) onTabChange("date");
};
const handleDateSubmit = (_data: { startDate: DateObject | null; endDate: DateObject | null }) => {
handleDateClose();
};
return (
<div className="relative">
<TaskDetailActionBar
@@ -130,6 +141,11 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
<TaskDetailAttachmentPopover onClose={handleAttachmentClose} onSubmit={handleAttachmentCreate} />
) : null
}
datePopover={
isDateOpen ? (
<TaskDetailDatePopover onClose={handleDateClose} onSubmit={handleDateSubmit} />
) : null
}
/>
<TaskDetailMetadataPreview selectedLabels={selectedLabels} selectedUsers={selectedUsers} />
@@ -7,11 +7,7 @@ type Props = {
};
const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit }) => {
return (
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
<TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} />
</div>
);
return <TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} />;
};
export default TaskDetailAttachmentPopover;
@@ -9,11 +9,7 @@ type Props = {
};
const TaskDetailChecklistPopover: FC<Props> = ({ checklists, onClose, onSubmit }) => {
return (
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
<TaskDetailChecklistForm checklists={checklists} onClose={onClose} onSubmit={onSubmit} />
</div>
);
return <TaskDetailChecklistForm checklists={checklists} onClose={onClose} onSubmit={onSubmit} />;
};
export default TaskDetailChecklistPopover;
@@ -0,0 +1,153 @@
import { type FC, useState } from "react";
import { useTranslation } from "react-i18next";
import DateObject from "react-date-object";
import persian from "react-date-object/calendars/persian";
import persian_fa from "react-date-object/locales/persian_fa";
import { Calendar } from "react-multi-date-picker";
import Button from "../../../../../components/Button";
import TaskDetailLabelCheckbox from "../labels/TaskDetailLabelCheckbox";
import type { TaskDateField } from "./types";
type Props = {
onClose: () => void;
onSubmit: (data: { startDate: DateObject | null; endDate: DateObject | null }) => void;
};
const formatDateLabel = (date: DateObject | null) => {
if (!date) return "";
return date.convert(persian, persian_fa).format("D MMMM");
};
const TaskDetailDateForm: FC<Props> = ({ onClose, onSubmit }) => {
const { t } = useTranslation("global");
const [startDate, setStartDate] = useState<DateObject | null>(null);
const [endDate, setEndDate] = useState<DateObject | null>(null);
const [startEnabled, setStartEnabled] = useState(true);
const [endEnabled, setEndEnabled] = useState(false);
const [activeField, setActiveField] = useState<TaskDateField>("start");
const calendarValue = (() => {
if (startEnabled && endEnabled && startDate && endDate) {
return [startDate, endDate];
}
if (activeField === "end" && endDate) return endDate;
if (startDate) return startDate;
return undefined;
})();
const handleCalendarChange = (value: DateObject | DateObject[] | null) => {
if (!value) return;
if (Array.isArray(value)) {
const [start, end] = value;
if (startEnabled) setStartDate(start);
if (endEnabled) setEndDate(end ?? null);
return;
}
if (activeField === "start" && startEnabled) {
setStartDate(value);
return;
}
if (activeField === "end" && endEnabled) {
setEndDate(value);
}
};
const handleStartToggle = () => {
const next = !startEnabled;
setStartEnabled(next);
if (next) {
setActiveField("start");
return;
}
if (!endEnabled) {
setEndEnabled(true);
setActiveField("end");
}
};
const handleEndToggle = () => {
const next = !endEnabled;
setEndEnabled(next);
if (next) {
setActiveField("end");
return;
}
if (!startEnabled) {
setStartEnabled(true);
setActiveField("start");
}
};
const canSubmit = (startEnabled && Boolean(startDate)) || (endEnabled && Boolean(endDate));
const handleSubmit = () => {
if (!canSubmit) return;
onSubmit({
startDate: startEnabled ? startDate : null,
endDate: endEnabled ? endDate : null,
});
};
return (
<div className="flex flex-col gap-3">
<div className="task-detail-date-calendar">
<Calendar
value={calendarValue}
onChange={handleCalendarChange}
calendar={persian}
locale={persian_fa}
range={startEnabled && endEnabled}
className="w-full"
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="text-xs shrink-0 w-[72px]">{t("taskmanager.task_detail.start_date")}</span>
<input
readOnly
value={formatDateLabel(startDate)}
placeholder={t("taskmanager.select_date")}
onFocus={() => setActiveField("start")}
className="flex-1 h-8 text-xs rounded-xl border border-white bg-white/63 px-3 text-[#292D32] outline-none"
/>
<TaskDetailLabelCheckbox
checked={startEnabled}
onToggle={handleStartToggle}
ariaLabel={t("taskmanager.task_detail.start_date")}
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs shrink-0 w-[72px]">{t("taskmanager.task_detail.end_date")}</span>
<input
readOnly
value={formatDateLabel(endDate)}
placeholder={t("taskmanager.select_date")}
onFocus={() => setActiveField("end")}
className="flex-1 h-8 text-xs rounded-xl border border-white bg-white/63 px-3 text-[#292D32] outline-none"
/>
<TaskDetailLabelCheckbox
checked={endEnabled}
onToggle={handleEndToggle}
ariaLabel={t("taskmanager.task_detail.end_date")}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button type="button" onClick={onClose} className="h-8 bg-[#E8E4F0] text-[#292D32] rounded-xl text-xs w-[95px]">
{t("cancel")}
</Button>
<Button type="button" onClick={handleSubmit} disabled={!canSubmit} className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]">
{t("taskmanager.task_detail.insert")}
</Button>
</div>
</div>
);
};
export default TaskDetailDateForm;
@@ -0,0 +1,14 @@
import { type FC } from "react";
import type DateObject from "react-date-object";
import TaskDetailDateForm from "./TaskDetailDateForm";
type Props = {
onClose: () => void;
onSubmit: (data: { startDate: DateObject | null; endDate: DateObject | null }) => void;
};
const TaskDetailDatePopover: FC<Props> = ({ onClose, onSubmit }) => {
return <TaskDetailDateForm onClose={onClose} onSubmit={onSubmit} />;
};
export default TaskDetailDatePopover;
@@ -0,0 +1,10 @@
import type DateObject from "react-date-object";
export type TaskDateField = "start" | "end";
export type TaskDateRange = {
startDate: DateObject | null;
endDate: DateObject | null;
startEnabled: boolean;
endEnabled: boolean;
};
@@ -21,7 +21,7 @@ const TaskDetailLabelsPopover: FC<Props> = ({
onCreate,
}) => {
return (
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
<>
{view === "list" ? (
<TaskDetailLabelsList
labels={labels}
@@ -36,7 +36,7 @@ const TaskDetailLabelsPopover: FC<Props> = ({
onSubmit={onCreate}
/>
)}
</div>
</>
);
};
@@ -9,11 +9,7 @@ type Props = {
};
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle }) => {
return (
<div className="absolute top-full right-0 mt-2 z-30 w-[300px] bg-white/90 backdrop-blur-md rounded-2xl p-4 shadow-lg border border-white/80">
<TaskDetailUsersList users={users} selectedIds={selectedIds} onToggle={onToggle} />
</div>
);
return <TaskDetailUsersList users={users} selectedIds={selectedIds} onToggle={onToggle} />;
};
export default TaskDetailUsersPopover;