manage users
This commit is contained in:
+48
-58
@@ -1,8 +1,8 @@
|
|||||||
import { FC, InputHTMLAttributes, useEffect, useState } from 'react'
|
import { SearchNormal } from "iconsax-react";
|
||||||
import { clx } from '../helpers/utils';
|
import { FC, InputHTMLAttributes, useEffect, useState } from "react";
|
||||||
import EyeIcon from '../assets/images/eye.svg'
|
import EyeIcon from "../assets/images/eye.svg";
|
||||||
import { SearchNormal } from 'iconsax-react';
|
import { clx } from "../helpers/utils";
|
||||||
import Error from './Error';
|
import Error from "./Error";
|
||||||
|
|
||||||
type Variant = "floating_outlined" | "primary" | "search";
|
type Variant = "floating_outlined" | "primary" | "search";
|
||||||
|
|
||||||
@@ -16,33 +16,31 @@ type Props = {
|
|||||||
seprator?: boolean;
|
seprator?: boolean;
|
||||||
isNotRequired?: boolean;
|
isNotRequired?: boolean;
|
||||||
onChangeSearchFinal?: (value: string) => void;
|
onChangeSearchFinal?: (value: string) => void;
|
||||||
} & InputHTMLAttributes<HTMLInputElement>
|
labelClassName?: string;
|
||||||
|
} & InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
const formatNumber = (value: string | number): string => {
|
const formatNumber = (value: string | number): string => {
|
||||||
if (!value) return '';
|
if (!value) return "";
|
||||||
const inputValue = String(value).replace(/,/g, '');
|
const inputValue = String(value).replace(/,/g, "");
|
||||||
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
};
|
};
|
||||||
|
|
||||||
const Input: FC<Props> = (props: Props) => {
|
const Input: FC<Props> = (props: Props) => {
|
||||||
|
const [formattedValue, setFormattedValue] = useState<string>(props.value ? formatNumber(props.value as string) : "");
|
||||||
|
|
||||||
const [formattedValue, setFormattedValue] = useState<string>(
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
props.value ? formatNumber(props.value as string) : ''
|
const [search, setSearch] = useState<string>("");
|
||||||
);
|
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
|
||||||
const [search, setSearch] = useState<string>('')
|
|
||||||
|
|
||||||
const inputClass = clx(
|
const inputClass = clx(
|
||||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
"w-full h-10 text-black block px-4 text-xs rounded-xl border border-border",
|
||||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
props.readOnly && "bg-gray-100 border-0 text-description",
|
||||||
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
|
props.variant === "search" && "bg-[#EEF0F7] border-0 ps-10",
|
||||||
props.className
|
props.className,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (props.seprator) {
|
if (props.seprator) {
|
||||||
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
|
const inputValue = event.target.value.replace(/,/g, ""); // حذف کاماها
|
||||||
const formatted = formatNumber(inputValue);
|
const formatted = formatNumber(inputValue);
|
||||||
|
|
||||||
// بهروزرسانی مقدار قالببندیشده
|
// بهروزرسانی مقدار قالببندیشده
|
||||||
@@ -65,51 +63,43 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
if (props.value) {
|
if (props.value) {
|
||||||
setFormattedValue(formatNumber(props.value as string));
|
setFormattedValue(formatNumber(props.value as string));
|
||||||
} else {
|
} else {
|
||||||
setFormattedValue('');
|
setFormattedValue("");
|
||||||
}
|
}
|
||||||
}, [props.value])
|
}, [props.value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.variant === 'search' && props.onChangeSearchFinal) {
|
if (props.variant === "search" && props.onChangeSearchFinal) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
props.onChangeSearchFinal?.(search)
|
props.onChangeSearchFinal?.(search);
|
||||||
}, 1000)
|
}, 1000);
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
}, [search, props])
|
}, [search, props]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className="w-full">
|
||||||
<label className='text-sm'>
|
<label className={clx("text-sm", props.labelClassName)}>{props.label}</label>
|
||||||
{props.label}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className='w-full relative mt-1'>
|
<div className="w-full relative mt-1">
|
||||||
<input {...props} onChange={(e) => {
|
<input
|
||||||
setSearch(e.target.value)
|
{...props}
|
||||||
handleInputChange(e)
|
onChange={(e) => {
|
||||||
}} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
setSearch(e.target.value);
|
||||||
|
handleInputChange(e);
|
||||||
{
|
}}
|
||||||
props.type === 'password' &&
|
value={props.seprator ? formattedValue : props.value}
|
||||||
<img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
|
type={props.type === "password" && showPassword ? "text" : props.type === "password" ? "password" : undefined}
|
||||||
}
|
className={inputClass}
|
||||||
|
|
||||||
{
|
|
||||||
props.variant === 'search' &&
|
|
||||||
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
props.error_text &&
|
|
||||||
<Error
|
|
||||||
errorText={props.error_text}
|
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Input
|
{props.type === "password" && <img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className="w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3" />}
|
||||||
|
|
||||||
|
{props.variant === "search" && <SearchNormal size={20} color="#8C90A3" className="absolute top-0 w-5 bottom-0 my-auto right-3" />}
|
||||||
|
|
||||||
|
{props.error_text && <Error errorText={props.error_text} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
||||||
|
|||||||
+2
-1
@@ -1000,7 +1000,8 @@
|
|||||||
"description_placeholder": "توضیحات خود را اضافه کنید",
|
"description_placeholder": "توضیحات خود را اضافه کنید",
|
||||||
"new_label": "برچسب جدید",
|
"new_label": "برچسب جدید",
|
||||||
"label_title": "عنوان",
|
"label_title": "عنوان",
|
||||||
"choose_custom_color": "انتخاب رنگ دلخواه"
|
"choose_custom_color": "انتخاب رنگ دلخواه",
|
||||||
|
"members": "اعضا"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ type Props = {
|
|||||||
activeTab: TaskDetailTab | null;
|
activeTab: TaskDetailTab | null;
|
||||||
onTabChange: (tab: TaskDetailTab) => void;
|
onTabChange: (tab: TaskDetailTab) => void;
|
||||||
labelsPopover?: ReactNode;
|
labelsPopover?: ReactNode;
|
||||||
|
usersPopover?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover }) => {
|
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover, usersPopover }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,11 +43,11 @@ const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (id === "labels") {
|
if (id === "labels" || id === "users") {
|
||||||
return (
|
return (
|
||||||
<div key={id} className="relative">
|
<div key={id} className="relative">
|
||||||
{button}
|
{button}
|
||||||
{labelsPopover}
|
{id === "labels" ? labelsPopover : usersPopover}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { type FC } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AvatarImage from "../../../../assets/images/avatar_image.png";
|
import AvatarImage from "../../../../assets/images/avatar_image.png";
|
||||||
import type { TaskLabel } from "./labels/types";
|
import type { TaskLabel } from "./labels/types";
|
||||||
|
import type { TaskUser } from "./users/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedLabels: TaskLabel[];
|
selectedLabels: TaskLabel[];
|
||||||
|
selectedUsers: TaskUser[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels }) => {
|
const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels, selectedUsers }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,10 +39,12 @@ const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium mb-2">{t("taskmanager.users")}</div>
|
<div className="text-xs font-medium mb-2">{t("taskmanager.users")}</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="size-8 rounded-full bg-[#D0D0D0] overflow-hidden">
|
{selectedUsers.map((user) => (
|
||||||
<img src={AvatarImage} alt="" className="size-full object-cover" />
|
<div key={user.id} className="size-8 rounded-full bg-[#D0D0D0] overflow-hidden">
|
||||||
|
<img src={user.avatar ?? AvatarImage} alt={user.name} className="size-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="size-7 rounded-full border border-[#D0D0D0] bg-white/60 flex items-center justify-center cursor-pointer"
|
className="size-7 rounded-full border border-[#D0D0D0] bg-white/60 flex items-center justify-center cursor-pointer"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { LabelsView, TaskLabel } from "./labels/types";
|
|||||||
import TaskDetailActionBar from "./TaskDetailActionBar";
|
import TaskDetailActionBar from "./TaskDetailActionBar";
|
||||||
import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
||||||
import type { TaskDetailTab } from "./types";
|
import type { TaskDetailTab } from "./types";
|
||||||
|
import { DEFAULT_USERS } from "./users/constants";
|
||||||
|
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeTab: TaskDetailTab | null;
|
activeTab: TaskDetailTab | null;
|
||||||
@@ -14,10 +16,13 @@ type Props = {
|
|||||||
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
||||||
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
||||||
const [labels, setLabels] = useState<TaskLabel[]>(DEFAULT_LABELS);
|
const [labels, setLabels] = useState<TaskLabel[]>(DEFAULT_LABELS);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(["1"]));
|
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set(["1"]));
|
||||||
|
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set(["1"]));
|
||||||
|
|
||||||
const selectedLabels = labels.filter((label) => selectedIds.has(label.id));
|
const selectedLabels = labels.filter((label) => selectedLabelIds.has(label.id));
|
||||||
|
const selectedUsers = DEFAULT_USERS.filter((user) => selectedUserIds.has(user.id));
|
||||||
const isLabelsOpen = activeTab === "labels";
|
const isLabelsOpen = activeTab === "labels";
|
||||||
|
const isUsersOpen = activeTab === "users";
|
||||||
|
|
||||||
const handleTabChange = (tab: TaskDetailTab) => {
|
const handleTabChange = (tab: TaskDetailTab) => {
|
||||||
if (tab === "labels" && activeTab !== "labels") {
|
if (tab === "labels" && activeTab !== "labels") {
|
||||||
@@ -26,8 +31,20 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
|||||||
onTabChange(tab);
|
onTabChange(tab);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = (id: string) => {
|
const handleLabelToggle = (id: string) => {
|
||||||
setSelectedIds((prev) => {
|
setSelectedLabelIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id);
|
||||||
|
} else {
|
||||||
|
next.add(id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserToggle = (id: string) => {
|
||||||
|
setSelectedUserIds((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(id)) {
|
if (next.has(id)) {
|
||||||
next.delete(id);
|
next.delete(id);
|
||||||
@@ -45,7 +62,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
|||||||
color,
|
color,
|
||||||
};
|
};
|
||||||
setLabels((prev) => [...prev, newLabel]);
|
setLabels((prev) => [...prev, newLabel]);
|
||||||
setSelectedIds((prev) => new Set([...prev, newLabel.id]));
|
setSelectedLabelIds((prev) => new Set([...prev, newLabel.id]));
|
||||||
setLabelsView("list");
|
setLabelsView("list");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,11 +72,25 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange }) => {
|
|||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
labelsPopover={
|
labelsPopover={
|
||||||
isLabelsOpen ? <TaskDetailLabelsPopover view={labelsView} labels={labels} selectedIds={selectedIds} onViewChange={setLabelsView} onToggle={handleToggle} onCreate={handleCreate} /> : null
|
isLabelsOpen ? (
|
||||||
|
<TaskDetailLabelsPopover
|
||||||
|
view={labelsView}
|
||||||
|
labels={labels}
|
||||||
|
selectedIds={selectedLabelIds}
|
||||||
|
onViewChange={setLabelsView}
|
||||||
|
onToggle={handleLabelToggle}
|
||||||
|
onCreate={handleCreate}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
usersPopover={
|
||||||
|
isUsersOpen ? (
|
||||||
|
<TaskDetailUsersPopover users={DEFAULT_USERS} selectedIds={selectedUserIds} onToggle={handleUserToggle} />
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TaskDetailMetadataPreview selectedLabels={selectedLabels} />
|
<TaskDetailMetadataPreview selectedLabels={selectedLabels} selectedUsers={selectedUsers} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { type FC, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Input from "../../../../../components/Input";
|
||||||
|
import type { TaskUser } from "./types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
users: TaskUser[];
|
||||||
|
selectedIds: Set<string>;
|
||||||
|
onToggle: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskDetailUsersList: FC<Props> = ({ users }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const filteredUsers = useMemo(() => {
|
||||||
|
const query = search.trim().toLowerCase();
|
||||||
|
if (!query) return users;
|
||||||
|
return users.filter((user) => user.name.toLowerCase().includes(query));
|
||||||
|
}, [users, search]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs font-bold text-center">{t("taskmanager.users")}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input label={t("search")} value={search} onChange={(e) => setSearch(e.target.value)} className="h-8 bg-white/63 border-white" labelClassName="text-xs" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.members")}</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{filteredUsers.map((user) => {
|
||||||
|
return (
|
||||||
|
<div key={user.id} className="flex gap-1.5 items-center">
|
||||||
|
<div className="size-6 rounded-full bg-gray-400"></div>
|
||||||
|
<div className="text-[10px]">{user.name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskDetailUsersList;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { type FC } from "react";
|
||||||
|
import TaskDetailUsersList from "./TaskDetailUsersList";
|
||||||
|
import type { TaskUser } from "./types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
users: TaskUser[];
|
||||||
|
selectedIds: Set<string>;
|
||||||
|
onToggle: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskDetailUsersPopover;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { TaskUser } from "./types";
|
||||||
|
|
||||||
|
export const DEFAULT_USERS: TaskUser[] = [
|
||||||
|
{ id: "1", name: "فروغ براتی" },
|
||||||
|
{ id: "2", name: "پارسا مهرزاد" },
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type TaskUser = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatar?: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user