From 9c6aa47cf7dcce8cad56b55eb7346434ab31c5a2 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 17 Jun 2026 16:14:18 +0330 Subject: [PATCH] Setting design --- src/index.css | 76 +++++++++ src/langs/fa.json | 4 +- .../task-detail/TaskDetailActionBar.tsx | 87 ++++++---- .../task-detail/TaskDetailPopoverPanel.tsx | 54 +++++++ .../task-detail/TaskDetailToolbar.tsx | 16 ++ .../TaskDetailAttachmentPopover.tsx | 6 +- .../checklist/TaskDetailChecklistPopover.tsx | 6 +- .../task-detail/date/TaskDetailDateForm.tsx | 153 ++++++++++++++++++ .../date/TaskDetailDatePopover.tsx | 14 ++ .../components/task-detail/date/types.ts | 10 ++ .../labels/TaskDetailLabelsPopover.tsx | 4 +- .../users/TaskDetailUsersPopover.tsx | 6 +- 12 files changed, 384 insertions(+), 52 deletions(-) create mode 100644 src/pages/taskmanager/components/task-detail/TaskDetailPopoverPanel.tsx create mode 100644 src/pages/taskmanager/components/task-detail/date/TaskDetailDateForm.tsx create mode 100644 src/pages/taskmanager/components/task-detail/date/TaskDetailDatePopover.tsx create mode 100644 src/pages/taskmanager/components/task-detail/date/types.ts diff --git a/src/index.css b/src/index.css index a3bba33..76b0389 100644 --- a/src/index.css +++ b/src/index.css @@ -72,6 +72,82 @@ tbody tr { .rmdp-container { 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 { direction: ltr; } diff --git a/src/langs/fa.json b/src/langs/fa.json index 0098b65..9e7dbd0 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -1006,7 +1006,9 @@ "none": "هیچ کدام", "link": "لینک", "or": "یا", - "insert": "درج" + "insert": "درج", + "start_date": "تاریخ شروع", + "end_date": "تاریخ پایان" } } } diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx index 8f067dc..16ec3f8 100644 --- a/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx +++ b/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx @@ -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 = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover }) => { +const POPOVER_TABS = new Set(["labels", "users", "checklist", "attachment", "date"]); + +type PopoverTab = "labels" | "users" | "checklist" | "attachment" | "date"; + +const TaskDetailActionBar: FC = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover, datePopover }) => { const { t } = useTranslation("global"); + const anchorRefs = useRef>({ + labels: { current: null }, + users: { current: null }, + checklist: { current: null }, + attachment: { current: null }, + date: { current: null }, + }); + + const popoverByTab: Record = { + labels: labelsPopover, + users: usersPopover, + checklist: checklistPopover, + attachment: attachmentPopover, + date: datePopover, + }; return ( -
- {TABS.map(({ id, icon: Icon, labelKey }) => { - const isActive = activeTab === id; + <> +
+ {TABS.map(({ id, icon: Icon, labelKey }) => { + const isActive = activeTab === id; + const hasPopover = POPOVER_TABS.has(id); - const button = ( - - ); - - if (id === "labels" || id === "users" || id === "checklist" || id === "attachment") { return ( -
- {button} - {id === "labels" - ? labelsPopover - : id === "users" - ? usersPopover - : id === "checklist" - ? checklistPopover - : attachmentPopover} +
{ + if (!hasPopover) return; + anchorRefs.current[id as PopoverTab].current = element; + }} + > +
); - } + })} +
- return ( -
- {button} -
- ); - })} -
+ {(Object.keys(popoverByTab) as PopoverTab[]).map((tab) => + popoverByTab[tab] ? ( + + {popoverByTab[tab]} + + ) : null, + )} + ); }; diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailPopoverPanel.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailPopoverPanel.tsx new file mode 100644 index 0000000..6e23bc9 --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/TaskDetailPopoverPanel.tsx @@ -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 = ({ 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( +
+ {children} +
, + document.body, + ); +}; + +export default TaskDetailPopoverPanel; diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx index bd006a2..1aa7b76 100644 --- a/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx +++ b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx @@ -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 = ({ 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 = ({ activeTab, onTabChange }) => { handleAttachmentClose(); }; + const handleDateClose = () => { + if (isDateOpen) onTabChange("date"); + }; + + const handleDateSubmit = (_data: { startDate: DateObject | null; endDate: DateObject | null }) => { + handleDateClose(); + }; + return (
= ({ activeTab, onTabChange }) => { ) : null } + datePopover={ + isDateOpen ? ( + + ) : null + } /> diff --git a/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentPopover.tsx b/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentPopover.tsx index d1076ef..9fc8fec 100644 --- a/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentPopover.tsx +++ b/src/pages/taskmanager/components/task-detail/attachment/TaskDetailAttachmentPopover.tsx @@ -7,11 +7,7 @@ type Props = { }; const TaskDetailAttachmentPopover: FC = ({ onClose, onSubmit }) => { - return ( -
- -
- ); + return ; }; export default TaskDetailAttachmentPopover; diff --git a/src/pages/taskmanager/components/task-detail/checklist/TaskDetailChecklistPopover.tsx b/src/pages/taskmanager/components/task-detail/checklist/TaskDetailChecklistPopover.tsx index 9fac04c..c18b61c 100644 --- a/src/pages/taskmanager/components/task-detail/checklist/TaskDetailChecklistPopover.tsx +++ b/src/pages/taskmanager/components/task-detail/checklist/TaskDetailChecklistPopover.tsx @@ -9,11 +9,7 @@ type Props = { }; const TaskDetailChecklistPopover: FC = ({ checklists, onClose, onSubmit }) => { - return ( -
- -
- ); + return ; }; export default TaskDetailChecklistPopover; diff --git a/src/pages/taskmanager/components/task-detail/date/TaskDetailDateForm.tsx b/src/pages/taskmanager/components/task-detail/date/TaskDetailDateForm.tsx new file mode 100644 index 0000000..bca4a54 --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/date/TaskDetailDateForm.tsx @@ -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 = ({ onClose, onSubmit }) => { + const { t } = useTranslation("global"); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const [startEnabled, setStartEnabled] = useState(true); + const [endEnabled, setEndEnabled] = useState(false); + const [activeField, setActiveField] = useState("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 ( +
+
+ +
+ +
+
+ {t("taskmanager.task_detail.start_date")} + setActiveField("start")} + className="flex-1 h-8 text-xs rounded-xl border border-white bg-white/63 px-3 text-[#292D32] outline-none" + /> + +
+ +
+ {t("taskmanager.task_detail.end_date")} + setActiveField("end")} + className="flex-1 h-8 text-xs rounded-xl border border-white bg-white/63 px-3 text-[#292D32] outline-none" + /> + +
+
+ +
+ + +
+
+ ); +}; + +export default TaskDetailDateForm; diff --git a/src/pages/taskmanager/components/task-detail/date/TaskDetailDatePopover.tsx b/src/pages/taskmanager/components/task-detail/date/TaskDetailDatePopover.tsx new file mode 100644 index 0000000..233d8ef --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/date/TaskDetailDatePopover.tsx @@ -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 = ({ onClose, onSubmit }) => { + return ; +}; + +export default TaskDetailDatePopover; diff --git a/src/pages/taskmanager/components/task-detail/date/types.ts b/src/pages/taskmanager/components/task-detail/date/types.ts new file mode 100644 index 0000000..55c8fb5 --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/date/types.ts @@ -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; +}; diff --git a/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx index 727797a..5daf2f8 100644 --- a/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx +++ b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx @@ -21,7 +21,7 @@ const TaskDetailLabelsPopover: FC = ({ onCreate, }) => { return ( -
+ <> {view === "list" ? ( = ({ onSubmit={onCreate} /> )} -
+ ); }; diff --git a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx index 84c8b2e..1ddc7f2 100644 --- a/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx +++ b/src/pages/taskmanager/components/task-detail/users/TaskDetailUsersPopover.tsx @@ -9,11 +9,7 @@ type Props = { }; const TaskDetailUsersPopover: FC = ({ users, selectedIds, onToggle }) => { - return ( -
- -
- ); + return ; }; export default TaskDetailUsersPopover;