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
+76
View File
@@ -72,6 +72,82 @@ tbody tr {
.rmdp-container { .rmdp-container {
width: 100%; width: 100%;
} }
.task-detail-date-calendar .rmdp-wrapper {
width: 100%;
box-shadow: none;
border: none;
background: transparent;
}
.task-detail-date-calendar .rmdp-calendar {
width: 100%;
padding: 0;
}
.task-detail-date-calendar .rmdp-header {
margin-bottom: 8px;
padding: 0;
}
.task-detail-date-calendar .rmdp-header-values {
font-size: 12px;
font-weight: 700;
color: #292d32;
}
.task-detail-date-calendar .rmdp-arrow {
border-color: #292d32;
width: 8px;
height: 8px;
}
.task-detail-date-calendar .rmdp-week-day {
font-size: 11px;
color: #8c90a3;
font-weight: 500;
}
.task-detail-date-calendar .rmdp-day {
width: 32px;
height: 32px;
}
.task-detail-date-calendar .rmdp-day span {
font-size: 11px;
border-radius: 8px;
}
.task-detail-date-calendar .rmdp-day.rmdp-deactive span {
color: #c4c4c4;
}
.task-detail-date-calendar .rmdp-day.rmdp-today span {
background-color: transparent;
color: #292d32;
font-weight: 700;
}
.task-detail-date-calendar .rmdp-day:not(.rmdp-disabled):not(.rmdp-day-hidden) span:hover {
background-color: #f0edf5;
color: #292d32;
}
.task-detail-date-calendar .rmdp-day.rmdp-selected span:not(.highlight) {
background-color: #292d32;
color: #fff;
box-shadow: none;
}
.task-detail-date-calendar .rmdp-range {
background-color: #f0edf5;
box-shadow: none;
}
.task-detail-date-calendar .rmdp-day.rmdp-range.start,
.task-detail-date-calendar .rmdp-day.rmdp-range.end {
background-color: transparent;
}
.dltr { .dltr {
direction: ltr; direction: ltr;
} }
+3 -1
View File
@@ -1006,7 +1006,9 @@
"none": "هیچ کدام", "none": "هیچ کدام",
"link": "لینک", "link": "لینک",
"or": "یا", "or": "یا",
"insert": "درج" "insert": "درج",
"start_date": "تاریخ شروع",
"end_date": "تاریخ پایان"
} }
} }
} }
@@ -1,6 +1,7 @@
import { Calendar, Paperclip2, Profile2User, Tag, TickSquare, type Icon } from "iconsax-react"; 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 { useTranslation } from "react-i18next";
import TaskDetailPopoverPanel from "./TaskDetailPopoverPanel";
import type { TaskDetailTab } from "./types"; import type { TaskDetailTab } from "./types";
type TabItem = { type TabItem = {
@@ -24,49 +25,67 @@ type Props = {
usersPopover?: ReactNode; usersPopover?: ReactNode;
checklistPopover?: ReactNode; checklistPopover?: ReactNode;
attachmentPopover?: 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 { 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 ( return (
<div className="flex flex-wrap items-center gap-2"> <>
{TABS.map(({ id, icon: Icon, labelKey }) => { <div className="flex flex-wrap items-center gap-2">
const isActive = activeTab === id; {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 ( return (
<div key={id} className="relative"> <div
{button} key={id}
{id === "labels" ref={(element) => {
? labelsPopover if (!hasPopover) return;
: id === "users" anchorRefs.current[id as PopoverTab].current = element;
? usersPopover }}
: id === "checklist" >
? checklistPopover <button
: attachmentPopover} 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>
); );
} })}
</div>
return ( {(Object.keys(popoverByTab) as PopoverTab[]).map((tab) =>
<div key={id}> popoverByTab[tab] ? (
{button} <TaskDetailPopoverPanel key={tab} anchorRef={anchorRefs.current[tab]}>
</div> {popoverByTab[tab]}
); </TaskDetailPopoverPanel>
})} ) : null,
</div> )}
</>
); );
}; };
@@ -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 { type FC, useState } from "react";
import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover"; import TaskDetailAttachmentPopover from "./attachment/TaskDetailAttachmentPopover";
import TaskDetailDatePopover from "./date/TaskDetailDatePopover";
import type DateObject from "react-date-object";
import { DEFAULT_CHECKLISTS } from "./checklist/constants"; import { DEFAULT_CHECKLISTS } from "./checklist/constants";
import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover"; import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover";
import type { TaskChecklist } from "./checklist/types"; import type { TaskChecklist } from "./checklist/types";
@@ -30,6 +32,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
const isUsersOpen = activeTab === "users"; const isUsersOpen = activeTab === "users";
const isChecklistOpen = activeTab === "checklist"; const isChecklistOpen = activeTab === "checklist";
const isAttachmentOpen = activeTab === "attachment"; const isAttachmentOpen = activeTab === "attachment";
const isDateOpen = activeTab === "date";
const handleTabChange = (tab: TaskDetailTab) => { const handleTabChange = (tab: TaskDetailTab) => {
if (tab === "labels" && activeTab !== "labels") { if (tab === "labels" && activeTab !== "labels") {
@@ -94,6 +97,14 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
handleAttachmentClose(); handleAttachmentClose();
}; };
const handleDateClose = () => {
if (isDateOpen) onTabChange("date");
};
const handleDateSubmit = (_data: { startDate: DateObject | null; endDate: DateObject | null }) => {
handleDateClose();
};
return ( return (
<div className="relative"> <div className="relative">
<TaskDetailActionBar <TaskDetailActionBar
@@ -130,6 +141,11 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
<TaskDetailAttachmentPopover onClose={handleAttachmentClose} onSubmit={handleAttachmentCreate} /> <TaskDetailAttachmentPopover onClose={handleAttachmentClose} onSubmit={handleAttachmentCreate} />
) : null ) : null
} }
datePopover={
isDateOpen ? (
<TaskDetailDatePopover onClose={handleDateClose} onSubmit={handleDateSubmit} />
) : null
}
/> />
<TaskDetailMetadataPreview selectedLabels={selectedLabels} selectedUsers={selectedUsers} /> <TaskDetailMetadataPreview selectedLabels={selectedLabels} selectedUsers={selectedUsers} />
@@ -7,11 +7,7 @@ type Props = {
}; };
const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit }) => { const TaskDetailAttachmentPopover: FC<Props> = ({ onClose, onSubmit }) => {
return ( return <TaskDetailAttachmentForm onClose={onClose} onSubmit={onSubmit} />;
<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>
);
}; };
export default TaskDetailAttachmentPopover; export default TaskDetailAttachmentPopover;
@@ -9,11 +9,7 @@ type Props = {
}; };
const TaskDetailChecklistPopover: FC<Props> = ({ checklists, onClose, onSubmit }) => { const TaskDetailChecklistPopover: FC<Props> = ({ checklists, onClose, onSubmit }) => {
return ( return <TaskDetailChecklistForm checklists={checklists} onClose={onClose} onSubmit={onSubmit} />;
<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>
);
}; };
export default TaskDetailChecklistPopover; 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, onCreate,
}) => { }) => {
return ( 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" ? ( {view === "list" ? (
<TaskDetailLabelsList <TaskDetailLabelsList
labels={labels} labels={labels}
@@ -36,7 +36,7 @@ const TaskDetailLabelsPopover: FC<Props> = ({
onSubmit={onCreate} onSubmit={onCreate}
/> />
)} )}
</div> </>
); );
}; };
@@ -9,11 +9,7 @@ type Props = {
}; };
const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle }) => { const TaskDetailUsersPopover: FC<Props> = ({ users, selectedIds, onToggle }) => {
return ( return <TaskDetailUsersList users={users} selectedIds={selectedIds} onToggle={onToggle} />;
<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>
);
}; };
export default TaskDetailUsersPopover; export default TaskDetailUsersPopover;