Update drawer + tag dropdown for proper functionality

This commit is contained in:
pmoharana-cmd 2025-01-03 15:18:09 -05:00
parent ad5d44520b
commit c1b055f2f8
11 changed files with 462 additions and 509 deletions

View File

@ -14,61 +14,56 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
type DrawerProps = { type InputType =
title: string; | "text"
children: ReactNode; | "email"
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; | "textarea"
type?: "button" | "submit" | "reset"; // specify possible values for type | "select-one"
disabled?: boolean; | "select-multiple";
editableContent?: any;
onSave?: (content: any) => void;
rowContent?: any;
setData: Dispatch<SetStateAction<any>>;
};
interface EditContent { export interface Details {
content: string; key: string;
isEditing: boolean; label: string;
inputType: InputType;
icon: ReactNode;
presetOptionsValues?: string[];
presetOptionsSetter?: Dispatch<SetStateAction<string[]>>;
} }
type DrawerProps = {
titleKey: string;
details: Details[];
rowContent?: any;
setRowContent?: Dispatch<SetStateAction<any>>;
};
const Drawer: FunctionComponent<DrawerProps> = ({ const Drawer: FunctionComponent<DrawerProps> = ({
title, titleKey,
children, details,
onSave,
editableContent,
rowContent, rowContent,
setData, setRowContent,
}) => { }: DrawerProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isFull, setIsFull] = useState(false); const [isFull, setIsFull] = useState(false);
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
const [tempRowContent, setTempRowContent] = useState(rowContent); const [tempRowContent, setTempRowContent] = useState(rowContent);
const onRowUpdate = (updatedRow: any) => { const onRowUpdate = (updatedRow: any) => {};
setData((prevData: any) =>
prevData.map((row: any) =>
row.id === updatedRow.id ? updatedRow : row
)
);
};
const handleTempRowContentChange = (e) => { const handleTempRowContentChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target; const { name, value } = e.target;
console.log(name); setTempRowContent((prev: any) => ({
console.log(value); ...prev,
setTempRowContent((prevContent) => ({
...prevContent,
[name]: value, [name]: value,
})); }));
}; };
const handleEnterPress = (e) => { const handleEnterPress = (
e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault();
// Update the rowContent with the temporaryRowContent
if (onRowUpdate) {
onRowUpdate(tempRowContent);
}
} }
}; };
@ -99,34 +94,6 @@ const Drawer: FunctionComponent<DrawerProps> = ({
<OutlineStarIcon className="h-5 w-5" /> <OutlineStarIcon className="h-5 w-5" />
); );
const [presetOptions, setPresetOptions] = useState([
"administrator",
"volunteer",
"employee",
]);
const [rolePresetOptions, setRolePresetOptions] = useState([
"domestic",
"community",
"economic",
]);
const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) {
const colors = [
"bg-cyan-100",
"bg-blue-100",
"bg-green-100",
"bg-yellow-100",
"bg-purple-100",
];
const randomColor =
colors[Math.floor(Math.random() * colors.length)];
setTagColors(new Map(tagColors).set(tag, randomColor));
}
return tagColors.get(tag);
};
return ( return (
<div> <div>
<button <button
@ -144,7 +111,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
<UserIcon /> <UserIcon />
</span> </span>
<h2 className="text-lg text-gray-800 font-semibold"> <h2 className="text-lg text-gray-800 font-semibold">
{rowContent.username} {rowContent[titleKey]}
</h2> </h2>
</div> </div>
<div> <div>
@ -169,71 +136,116 @@ const Drawer: FunctionComponent<DrawerProps> = ({
</div> </div>
</div> </div>
<div className="p-4"> <div className="p-4">
<table className="p-4"> <div className="flex flex-col space-y-3">
<tbody className="items-center"> {details.map((detail, index) => {
<tr className="w-full text-xs items-center flex flex-row justify-between"> const value = tempRowContent[detail.key];
<td className="flex flex-row space-x-2 text-gray-500 items-center"> let valueToRender = <></>;
<UserIcon className="h-4 w-4" />
<span className="w-32">Username</span> switch (detail.inputType) {
</td> case "select-one":
<td className="w-3/4 p-2 pl-0"> case "select-multiple":
<input valueToRender = (
type="text" <div className="flex-1">
name="username" <div className="hover:bg-gray-50 rounded-md px-2 py-1">
value={tempRowContent.username} <TagsInput
onChange={handleTempRowContentChange} presetValue={
onKeyDown={handleEnterPress} typeof value ===
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50" "string"
/> ? [value]
</td> : value
</tr> }
<tr className="w-full text-xs items-center flex flex-row justify-between"> presetOptions={
<td className="flex flex-row space-x-2 text-gray-500 items-center"> detail.presetOptionsValues ||
<ListBulletIcon className="h-4 w-4" /> []
</td> }
<td className="w-32">Role</td> setPresetOptions={
<td className="w-3/4 hover:bg-gray-50"> detail.presetOptionsSetter ||
<TagsInput (() => {})
presetValue={tempRowContent.role} }
presetOptions={presetOptions} />
setPresetOptions={setPresetOptions} </div>
getTagColor={getTagColor} </div>
/> );
</td> break;
</tr> case "textarea":
<tr className="w-full text-xs items-center flex flex-row justify-between"> valueToRender = (
<td className="flex flex-row space-x-2 text-gray-500 items-center"> <div className="flex-1">
<EnvelopeIcon className="h-4 w-4" /> <div className="hover:bg-gray-50 rounded-md px-2 py-1">
</td> <textarea
<td className="w-32">Email</td> name={detail.key}
<td className="w-3/4 p-2 pl-0"> value={value}
<input onChange={
type="text" handleTempRowContentChange
name="email" }
value={tempRowContent.email} onKeyDown={handleEnterPress}
onChange={handleTempRowContentChange} rows={4}
onKeyDown={handleEnterPress} onInput={(e) => {
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500" const target =
/> e.target as HTMLTextAreaElement;
</td> target.style.height =
</tr> "auto";
<tr className="w-full text-xs items-center flex flex-row justify-between"> target.style.height =
<td className="flex flex-row space-x-2 text-gray-500 items-center"> target.scrollHeight +
<ListBulletIcon className="h-4 w-4" /> "px";
</td> }}
<td className="w-32">Type of Program</td> className="w-full p-2 focus:outline-none border border-gray-200 rounded-md resize-none font-normal bg-transparent"
<td className="w-3/4 hover:bg-gray-50"> />
{/* {rowContent.program} */} </div>
<TagsInput </div>
presetValue={tempRowContent.program} );
presetOptions={rolePresetOptions} break;
setPresetOptions={setRolePresetOptions} case "text":
getTagColor={getTagColor} valueToRender = (
/> <div className="flex-1">
</td> <div className="hover:bg-gray-50 rounded-md px-2 py-1">
</tr> <input
</tbody> type={detail.inputType}
</table> name={detail.key}
value={value}
onChange={
handleTempRowContentChange
}
onKeyDown={handleEnterPress}
className="w-full p-1 focus:outline-gray-200 bg-transparent"
/>
</div>
</div>
);
break;
case "email":
valueToRender = (
<div className="flex-1">
<div className="hover:bg-gray-50 rounded-md px-2 py-1">
<input
type={detail.inputType}
name={detail.key}
value={value}
onChange={
handleTempRowContentChange
}
onKeyDown={handleEnterPress}
className="w-full p-1 font-normal hover:text-gray-400 focus:outline-gray-200 underline text-gray-500 bg-transparent"
/>
</div>
</div>
);
break;
}
return (
<div
key={index}
className="flex items-center text-xs gap-3"
>
<div className="flex items-center text-gray-500">
{detail.icon}
</div>
<div className="w-32">{detail.label}</div>
{valueToRender}
</div>
);
})}
</div>
<br /> <br />
</div> </div>
</div> </div>

View File

@ -1,4 +1,10 @@
import { Bars2Icon } from "@heroicons/react/24/solid"; import {
Bars2Icon,
DocumentTextIcon,
LinkIcon,
ListBulletIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler"; import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
@ -6,6 +12,8 @@ import { RowOpenAction } from "@/components/Table/RowOpenAction";
import Table from "@/components/Table/Table"; import Table from "@/components/Table/Table";
import TagsInput from "@/components/TagsInput/Index"; import TagsInput from "@/components/TagsInput/Index";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag";
type ResourceTableProps = { type ResourceTableProps = {
data: Resource[]; data: Resource[];
@ -20,60 +28,101 @@ type ResourceTableProps = {
export default function ResourceTable({ data, setData }: ResourceTableProps) { export default function ResourceTable({ data, setData }: ResourceTableProps) {
const columnHelper = createColumnHelper<Resource>(); const columnHelper = createColumnHelper<Resource>();
// Set up tag handling const [programPresets, setProgramPresets] = useState([
const programProps = useTagsHandler(["community", "domestic", "economic"]); "domestic",
"community",
"economic",
]);
const resourceDetails: Details[] = [
{
key: "name",
label: "name",
inputType: "text",
icon: <UserIcon className="h-4 w-4" />,
},
{
key: "link",
label: "link",
inputType: "email",
icon: <LinkIcon className="h-4 w-4" />,
},
{
key: "program",
label: "program",
inputType: "select-one",
icon: <ListBulletIcon className="h-4 w-4" />,
presetOptionsValues: programPresets,
presetOptionsSetter: setProgramPresets,
},
{
key: "summary",
label: "summary",
inputType: "textarea",
icon: <DocumentTextIcon className="h-4 w-4" />,
},
];
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<Resource, any>[] = [ const columns: ColumnDef<Resource, any>[] = [
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Name <UserIcon className="inline align-top h-4" /> Name
</> </>
), ),
cell: (info) => ( cell: (info) => (
<RowOpenAction <RowOpenAction
title={info.getValue()} title={info.getValue()}
titleKey="name"
rowData={info.row.original} rowData={info.row.original}
setData={setData} setData={setData}
details={resourceDetails}
/> />
), ),
}), }),
columnHelper.accessor("link", { columnHelper.accessor("link", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Link <LinkIcon className="inline align-top h-4" /> Link
</> </>
), ),
cell: (info) => ( cell: (info) => (
<a <div className="flex items-start gap-2 px-2">
href={info.getValue()} <a
target={"_blank"} href={info.getValue()}
className="ml-2 text-gray-500 underline hover:text-gray-400" target="_blank"
> className="text-gray-500 underline hover:text-gray-400 break-all"
{info.getValue()} >
</a> {info.getValue()}
</a>
</div>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Program <ListBulletIcon className="inline align-top h-4" /> Program
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...programProps} /> <div className="flex flex-wrap gap-2 items-center px-2">
<Tag>{info.getValue()}</Tag>
</div>
), ),
}), }),
columnHelper.accessor("summary", { columnHelper.accessor("summary", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Summary <DocumentTextIcon className="inline align-top h-4" />{" "}
Summary
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span> <div className="flex items-start gap-2 px-2 py-1">
<span className="text-gray-500 max-h-8 overflow-y-auto">
{info.getValue()}
</span>
</div>
), ),
}), }),
]; ];

View File

@ -1,37 +1,37 @@
import Drawer from "@/components/Drawer/Drawer"; import Drawer, { Details } from "@/components/Drawer/Drawer";
import DataPoint from "@/utils/models/DataPoint"; import DataPoint from "@/utils/models/DataPoint";
import {
EnvelopeIcon,
ListBulletIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
type RowOpenActionProps<T extends DataPoint> = { type RowOpenActionProps<T extends DataPoint> = {
title: string; title: string;
titleKey: string;
rowData: T; rowData: T;
setData: Dispatch<SetStateAction<T[]>>; setData: Dispatch<SetStateAction<T[]>>;
details: Details[];
}; };
export function RowOpenAction<T extends DataPoint>({ export function RowOpenAction<T extends DataPoint>({
title, title,
titleKey,
rowData, rowData,
setData, setData,
details,
}: RowOpenActionProps<T>) { }: RowOpenActionProps<T>) {
const [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent: string) => {
setPageContent(newContent);
};
return ( return (
<div className="font-semibold group flex flex-row items-center justify-between pr-2"> <div className="font-semibold group flex flex-row items-center justify-between pr-2">
{title} {title}
<span> <span>
<Drawer <Drawer
title="My Drawer Title" titleKey={titleKey}
editableContent={pageContent}
rowContent={rowData} rowContent={rowData}
onSave={handleDrawerContentChange} details={details}
setData={setData} setRowContent={setData}
> />
{pageContent}
</Drawer>
</span> </span>
</div> </div>
); );

View File

@ -1,11 +1,17 @@
import { Bars2Icon } from "@heroicons/react/24/solid"; import {
import { Dispatch, SetStateAction } from "react"; Bars2Icon,
import useTagsHandler from "@/components/TagsInput/TagsHandler"; CheckCircleIcon,
DocumentTextIcon,
ListBulletIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import Table from "@/components/Table/Table"; import Table from "@/components/Table/Table";
import { RowOpenAction } from "@/components/Table/RowOpenAction"; import { RowOpenAction } from "@/components/Table/RowOpenAction";
import TagsInput from "@/components/TagsInput/Index";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag";
type ServiceTableProps = { type ServiceTableProps = {
data: Service[]; data: Service[];
@ -20,11 +26,13 @@ type ServiceTableProps = {
export default function ServiceTable({ data, setData }: ServiceTableProps) { export default function ServiceTable({ data, setData }: ServiceTableProps) {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
// Set up tag handling const [programPresets, setProgramPresets] = useState([
const programProps = useTagsHandler(["community", "domestic", "economic"]); "domestic",
"community",
"economic",
]);
// TODO: Dynamically or statically get full list of preset requirement tag options const [requirementPresets, setRequirementPresets] = useState([
const requirementProps = useTagsHandler([
"anonymous", "anonymous",
"confidential", "confidential",
"referral required", "referral required",
@ -34,67 +42,111 @@ export default function ServiceTable({ data, setData }: ServiceTableProps) {
"initial assessment", "initial assessment",
]); ]);
const serviceDetails: Details[] = [
{
key: "name",
label: "name",
inputType: "text",
icon: <UserIcon className="inline align-top h-4" />,
},
{
key: "status",
label: "status",
inputType: "text",
icon: <CheckCircleIcon className="inline align-top h-4" />,
},
{
key: "program",
label: "program",
inputType: "select-one",
icon: <ListBulletIcon className="inline align-top h-4" />,
presetOptionsValues: programPresets,
presetOptionsSetter: setProgramPresets,
},
{
key: "requirements",
label: "requirements",
inputType: "select-multiple",
icon: <ListBulletIcon className="inline align-top h-4" />,
presetOptionsValues: requirementPresets,
presetOptionsSetter: setRequirementPresets,
},
{
key: "summary",
label: "summary",
inputType: "textarea",
icon: <DocumentTextIcon className="inline align-top h-4" />,
},
];
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<Service, any>[] = [ const columns: ColumnDef<Service, any>[] = [
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Name <UserIcon className="inline align-top h-4" /> Name
</> </>
), ),
cell: (info) => ( cell: (info) => (
<RowOpenAction <RowOpenAction
title={info.getValue()} title={info.getValue()}
titleKey="name"
rowData={info.row.original} rowData={info.row.original}
setData={setData} setData={setData}
details={serviceDetails}
/> />
), ),
}), }),
columnHelper.accessor("status", { columnHelper.accessor("status", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Status <CheckCircleIcon className="inline align-top h-4" /> Status
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span> <span className="text-gray-500 px-2">{info.getValue()}</span>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Program <ListBulletIcon className="inline align-top h-4" /> Program
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...programProps} /> <div className="flex flex-wrap gap-2 items-center px-2">
<Tag>{info.getValue()}</Tag>
</div>
), ),
}), }),
columnHelper.accessor("requirements", { columnHelper.accessor("requirements", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Requirements <ListBulletIcon className="inline align-top h-4" />{" "}
Requirements
</> </>
), ),
cell: (info) => ( cell: (info) => (
// TODO: Setup different tag handler for requirements <div className="flex flex-wrap gap-2 items-center px-2">
<TagsInput {info.getValue().map((tag: string, index: number) => {
presetValue={ return <Tag key={index}>{tag}</Tag>;
info.getValue()[0] !== "" ? info.getValue() : ["N/A"] })}
} </div>
{...requirementProps}
/>
), ),
}), }),
columnHelper.accessor("summary", { columnHelper.accessor("summary", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Summary <DocumentTextIcon className="inline align-top h-4" />{" "}
Summary
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span> <div className="flex items-start gap-2 px-2 py-1">
<span className="text-gray-500 max-h-8 overflow-y-auto">
{info.getValue()}
</span>
</div>
), ),
}), }),
]; ];

View File

@ -2,14 +2,19 @@ import {
ArrowDownCircleIcon, ArrowDownCircleIcon,
AtSymbolIcon, AtSymbolIcon,
Bars2Icon, Bars2Icon,
EnvelopeIcon,
ListBulletIcon,
UserIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler"; import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import Table from "@/components/Table/Table"; import Table from "@/components/Table/Table";
import { RowOpenAction } from "@/components/Table/RowOpenAction"; import { RowOpenAction } from "@/components/Table/RowOpenAction";
import TagsInput from "@/components/TagsInput/Index"; import TagsInput from "@/components/TagsInput/Index";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
import { Details } from "../Drawer/Drawer";
import { Tag } from "../TagsInput/Tag";
type UserTableProps = { type UserTableProps = {
data: User[]; data: User[];
@ -24,46 +29,83 @@ type UserTableProps = {
export default function UserTable({ data, setData }: UserTableProps) { export default function UserTable({ data, setData }: UserTableProps) {
const columnHelper = createColumnHelper<User>(); const columnHelper = createColumnHelper<User>();
// Set up tag handling const [rolePresets, setRolePresets] = useState([
const roleProps = useTagsHandler([ "admin",
"administrator",
"volunteer", "volunteer",
"employee", "employee",
]); ]);
const programProps = useTagsHandler(["community", "domestic", "economic"]); const [programPresets, setProgramPresets] = useState([
"domestic",
"community",
"economic",
]);
const userDetails: Details[] = [
{
key: "username",
label: "username",
inputType: "text",
icon: <UserIcon className="h-4 w-4" />,
},
{
key: "role",
label: "role",
inputType: "select-one",
icon: <ListBulletIcon className="h-4 w-4" />,
presetOptionsValues: rolePresets,
presetOptionsSetter: setRolePresets,
},
{
key: "email",
label: "email",
inputType: "email",
icon: <EnvelopeIcon className="h-4 w-4" />,
},
{
key: "program",
label: "program",
inputType: "select-multiple",
icon: <ListBulletIcon className="h-4 w-4" />,
presetOptionsValues: programPresets,
presetOptionsSetter: setProgramPresets,
},
];
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<User, any>[] = [ const columns: ColumnDef<User, any>[] = [
columnHelper.accessor("username", { columnHelper.accessor("username", {
header: () => ( header: () => (
<> <>
<Bars2Icon className="inline align-top h-4" /> Username <UserIcon className="inline align-top h-4" /> Username
</> </>
), ),
cell: (info) => ( cell: (info) => (
<RowOpenAction <RowOpenAction
title={info.getValue()} title={info.getValue()}
titleKey="username"
rowData={info.row.original} rowData={info.row.original}
setData={setData} setData={setData}
details={userDetails}
/> />
), ),
}), }),
columnHelper.accessor("role", { columnHelper.accessor("role", {
header: () => ( header: () => (
<> <>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "} <ListBulletIcon className="inline align-top h-4" /> Role
Role
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...roleProps} /> <div className="flex ml-2 flex-wrap gap-2 items-center">
<Tag>{info.getValue()}</Tag>
</div>
), ),
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
header: () => ( header: () => (
<> <>
<AtSymbolIcon className="inline align-top h-4" /> Email <EnvelopeIcon className="inline align-top h-4" /> Email
</> </>
), ),
cell: (info) => ( cell: (info) => (
@ -75,12 +117,15 @@ export default function UserTable({ data, setData }: UserTableProps) {
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "} <ListBulletIcon className="inline align-top h-4" /> Program
Program
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput presetValue={info.getValue()} {...programProps} /> <div className="flex ml-2 flex-wrap gap-2 items-center">
{info.getValue().map((tag: string, index: number) => {
return <Tag key={index}>{tag}</Tag>;
})}
</div>
), ),
}), }),
]; ];

View File

@ -1,12 +1,18 @@
import { Tag } from "./Tag"; import { Tag } from "./Tag";
export const CreateNewTagAction = ({ input }) => { interface NewTagProps {
input: string;
addTag: () => void;
}
export const CreateNewTagAction = ({ input, addTag }: NewTagProps) => {
return ( return (
<div className="flex flex-row space-x-2 hover:bg-gray-100 rounded-md py-2 p-2 items-center"> <button
className="flex flex-row space-x-2 hover:bg-gray-100 rounded-md py-2 p-2 items-center"
onClick={addTag}
>
<p className="capitalize">Create</p> <p className="capitalize">Create</p>
<Tag active={false} onDelete={null}> <Tag active={false}>{input}</Tag>
{input} </button>
</Tag>
</div>
); );
}; };

View File

@ -1,11 +1,21 @@
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid"; import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
import { useState } from "react"; import { useState } from "react";
export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => { interface DropdownActionProps {
tag: string;
handleDeleteTag: (tag: string) => void;
handleEditTag: (oldTag: string, newTag: string) => void;
}
export const DropdownAction = ({
tag,
handleDeleteTag,
handleEditTag,
}: DropdownActionProps) => {
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const [inputValue, setInputValue] = useState(tag); const [inputValue, setInputValue] = useState(tag);
const editTagOption = (e) => { const editTagOption = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleEditTag(tag, inputValue); handleEditTag(tag, inputValue);
setVisible(false); setVisible(false);

View File

@ -6,51 +6,53 @@ import { CreateNewTagAction } from "./CreateNewTagAction";
interface TagsInputProps { interface TagsInputProps {
presetOptions: string[]; presetOptions: string[];
presetValue: string | string[]; presetValue: string[];
setPresetOptions: Dispatch<SetStateAction<string[]>>; setPresetOptions: Dispatch<SetStateAction<string[]>>;
getTagColor(tag: string): string;
} }
const TagsInput: React.FC<TagsInputProps> = ({ const TagsInput: React.FC<TagsInputProps> = ({
presetValue, presetValue,
presetOptions, presetOptions,
setPresetOptions, setPresetOptions,
getTagColor,
}) => { }) => {
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [cellSelected, setCellSelected] = useState(false); const [cellSelected, setCellSelected] = useState(false);
// TODO: Add tags to the database and remove the presetValue and lowercasing
const [tags, setTags] = useState<Set<string>>( const [tags, setTags] = useState<Set<string>>(
typeof presetValue === "string" new Set(presetValue.map((tag) => tag.toLowerCase()))
? new Set([presetValue]) );
: new Set(presetValue) const [options, setOptions] = useState<Set<string>>(
new Set(presetOptions.map((option) => option.toLowerCase()))
);
const [filteredOptions, setFilteredOptions] = useState<Set<string>>(
new Set(presetOptions)
); );
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
const dropdown = useRef<HTMLDivElement>(null); const dropdown = useRef<HTMLDivElement>(null);
const handleClick = () => { const handleClick = () => {
if (!cellSelected) { if (!cellSelected) {
setCellSelected(true); setCellSelected(true);
// Add event listener only after setting cellSelected to true
setTimeout(() => { setTimeout(() => {
window.addEventListener("click", handleOutsideClick); window.addEventListener("click", handleOutsideClick);
}, 100); }, 100);
} }
}; };
// TODO: Fix MouseEvent type and remove the as Node as that is completely wrong
const handleOutsideClick = (event: MouseEvent) => { const handleOutsideClick = (event: MouseEvent) => {
console.log(dropdown.current);
if ( if (
dropdown.current && dropdown.current &&
!dropdown.current.contains(event.target as Node) !dropdown.current.contains(event.target as Node)
) { ) {
console.log("outside");
setCellSelected(false); setCellSelected(false);
// Remove event listener after handling outside click
window.removeEventListener("click", handleOutsideClick); window.removeEventListener("click", handleOutsideClick);
} }
}; };
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions(() => { setFilteredOptions(() => {
const newOptions = presetOptions.filter((item) => const newOptions = presetOptions.filter((item) =>
item.includes(e.target.value.toLowerCase()) item.includes(e.target.value.toLowerCase())
); );
@ -61,39 +63,45 @@ const TagsInput: React.FC<TagsInputProps> = ({
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && inputValue.trim()) { if (e.key === "Enter" && inputValue.trim()) {
// setPresetOptions((prevPreset) => { addTag(e);
// const uniqueSet = new Set(presetOptions);
// uniqueSet.add(inputValue);
// return Array.from(uniqueSet);
// });
setTags((prevTags) => new Set(prevTags).add(inputValue));
setOptions((prevOptions) => new Set(prevOptions).add(inputValue));
setInputValue("");
} }
}; };
const addTag = (e?: React.KeyboardEvent<HTMLInputElement>) => {
e?.stopPropagation();
setOptions(new Set(Array.from(options).concat(inputValue)));
setTags(new Set(Array.from(tags).concat(inputValue)));
setFilteredOptions(new Set(Array.from(options).concat(inputValue)));
setInputValue("");
};
const handleSelectTag = (tagToAdd: string) => { const handleSelectTag = (tagToAdd: string) => {
console.log(tagToAdd);
console.log(tags);
if (!tags.has(tagToAdd)) { if (!tags.has(tagToAdd)) {
// Corrected syntax for checking if a Set contains an item setTags(new Set(Array.from(tags).concat(tagToAdd)));
setTags((prevTags) => new Set(prevTags).add(tagToAdd));
} }
}; };
const handleDeleteTag = (tagToDelete: string) => { const handleDeleteTag = (tagToDelete: string) => {
setTags((prevTags) => { setTags(new Set(Array.from(tags).filter((tag) => tag !== tagToDelete)));
const updatedTags = new Set(prevTags);
updatedTags.delete(tagToDelete);
return updatedTags;
});
}; };
const handleDeleteTagOption = (tagToDelete: string) => { const handleDeleteTagOption = (tagToDelete: string) => {
// setPresetOptions(presetOptions.filter(tag => tag !== tagToDelete)); setOptions(
setOptions((prevOptions) => { new Set(
const updatedOptions = new Set(prevOptions); Array.from(options).filter((option) => option !== tagToDelete)
updatedOptions.delete(tagToDelete); )
return updatedOptions; );
});
setFilteredOptions(
new Set(
Array.from(options).filter((option) => option !== tagToDelete)
)
);
if (tags.has(tagToDelete)) { if (tags.has(tagToDelete)) {
handleDeleteTag(tagToDelete); handleDeleteTag(tagToDelete);
} }
@ -101,23 +109,20 @@ const TagsInput: React.FC<TagsInputProps> = ({
const handleEditTag = (oldTag: string, newTag: string) => { const handleEditTag = (oldTag: string, newTag: string) => {
if (oldTag !== newTag) { if (oldTag !== newTag) {
setTags((prevTags) => { setTags(
const tagsArray = Array.from(prevTags); new Set(
const oldTagIndex = tagsArray.indexOf(oldTag); Array.from(tags)
if (oldTagIndex !== -1) { .filter((tag) => tag !== oldTag)
tagsArray.splice(oldTagIndex, 1, newTag); .concat(newTag)
} )
return new Set(tagsArray); );
}); setOptions(
new Set(
setOptions((prevOptions) => { Array.from(options)
const optionsArray = Array.from(prevOptions); .filter((option) => option !== oldTag)
const oldTagIndex = optionsArray.indexOf(oldTag); .concat(newTag)
if (oldTagIndex !== -1) { )
optionsArray.splice(oldTagIndex, 1, newTag); );
}
return new Set(optionsArray);
});
} }
}; };
@ -157,10 +162,13 @@ const TagsInput: React.FC<TagsInputProps> = ({
handleDeleteTag={handleDeleteTagOption} handleDeleteTag={handleDeleteTagOption}
handleEditTag={handleEditTag} handleEditTag={handleEditTag}
handleAdd={handleSelectTag} handleAdd={handleSelectTag}
tags={options} tags={filteredOptions}
/> />
{inputValue.length > 0 && ( {inputValue.length > 0 && (
<CreateNewTagAction input={inputValue} /> <CreateNewTagAction
input={inputValue}
addTag={addTag}
/>
)} )}
</div> </div>
</div> </div>

View File

@ -1,14 +1,25 @@
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
export const Tag = ({ children, handleDelete, active = false }) => { interface TagProps {
children: string;
handleDelete?: (tag: string) => void;
active?: boolean;
}
export const Tag = ({ children, handleDelete, active = false }: TagProps) => {
return ( return (
<span <span
className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`} className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`}
> >
{children} {children}
{active && handleDelete && ( {active && handleDelete && (
<button onClick={() => handleDelete(children)}> <button
onClick={(e) => {
e.stopPropagation();
handleDelete(children);
}}
>
<XMarkIcon className={`ml-1 w-3 text-purple-500`} /> <XMarkIcon className={`ml-1 w-3 text-purple-500`} />
</button> </button>
)} )}

View File

@ -1,12 +1,19 @@
import { Tag } from "./Tag"; import { Tag } from "./Tag";
import { DropdownAction } from "./DropdownAction"; import { DropdownAction } from "./DropdownAction";
interface TagDropdownProps {
tags: Set<string>;
handleEditTag: (oldTag: string, newTag: string) => void;
handleDeleteTag: (tag: string) => void;
handleAdd: (tag: string) => void;
}
export const TagDropdown = ({ export const TagDropdown = ({
tags, tags,
handleEditTag, handleEditTag,
handleDeleteTag, handleDeleteTag,
handleAdd, handleAdd,
}) => { }: TagDropdownProps) => {
return ( return (
<div className="z-50 flex flex-col space-y-2 mt-2"> <div className="z-50 flex flex-col space-y-2 mt-2">
{Array.from(tags).map((tag, index) => ( {Array.from(tags).map((tag, index) => (

View File

@ -1,247 +0,0 @@
import { FunctionComponent, ReactNode } from "react";
import React, { useState } from "react";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
import {
StarIcon as SolidStarIcon,
EnvelopeIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import {
ArrowsPointingOutIcon,
ArrowsPointingInIcon,
StarIcon as OutlineStarIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline";
import TagsInput from "../TagsInput/Index";
type DrawerProps = {
title: string;
children: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
type?: "button" | "submit" | "reset"; // specify possible values for type
disabled?: boolean;
editableContent?: any;
onSave?: (content: any) => void;
rowContent?: any;
onRowUpdate?: (content: any) => void;
};
interface EditContent {
content: string;
isEditing: boolean;
}
const Drawer: FunctionComponent<DrawerProps> = ({
title,
children,
onSave,
editableContent,
rowContent,
onRowUpdate,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isFull, setIsFull] = useState(false);
const [isFavorite, setIsFavorite] = useState(false);
const [tempRowContent, setTempRowContent] = useState(rowContent);
const handleTempRowContentChange = (e) => {
const { name, value } = e.target;
console.log(name);
console.log(value);
setTempRowContent((prevContent) => ({
...prevContent,
[name]: value,
}));
};
const handleEnterPress = (e) => {
if (e.key === "Enter") {
e.preventDefault();
// Update the rowContent with the temporaryRowContent
if (onRowUpdate) {
onRowUpdate(tempRowContent);
}
}
};
const toggleDrawer = () => {
setIsOpen(!isOpen);
if (isFull) {
setIsFull(!isFull);
}
};
const toggleDrawerFullScreen = () => setIsFull(!isFull);
const toggleFavorite = () => setIsFavorite(!isFavorite);
const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
} ${isFull ? "w-full" : "w-1/2"}`;
const iconComponent = isFull ? (
<ArrowsPointingInIcon className="h-5 w-5" />
) : (
<ArrowsPointingOutIcon className="h-5 w-5" />
);
const favoriteIcon = isFavorite ? (
<SolidStarIcon className="h-5 w-5" />
) : (
<OutlineStarIcon className="h-5 w-5" />
);
const [presetOptions, setPresetOptions] = useState([
"administrator",
"volunteer",
"employee",
]);
const [rolePresetOptions, setRolePresetOptions] = useState([
"domestic",
"community",
"economic",
]);
const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) {
const colors = [
"bg-cyan-100",
"bg-blue-100",
"bg-green-100",
"bg-yellow-100",
"bg-purple-100",
];
const randomColor =
colors[Math.floor(Math.random() * colors.length)];
setTagColors(new Map(tagColors).set(tag, randomColor));
}
return tagColors.get(tag);
};
return (
<div>
<button
className={
"ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md"
}
onClick={toggleDrawer}
>
Open
</button>
<div className={drawerClassName}></div>
<div className={drawerClassName}>
<div className="flex items-center justify-between p-4">
<div className="flex flex-row items-center justify-between space-x-2">
<span className="h-5 text-purple-200 w-5">
<UserIcon />
</span>
<h2 className="text-lg text-gray-800 font-semibold">
{rowContent.username}
</h2>
</div>
<div>
<button
onClick={toggleFavorite}
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
>
{favoriteIcon}
</button>
<button
onClick={toggleDrawerFullScreen}
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
>
{iconComponent}
</button>
<button
onClick={toggleDrawer}
className="py-2 text-gray-500 hover:text-gray-800"
>
<ChevronDoubleLeftIcon className="h-5 w-5" />
</button>
</div>
</div>
<div className="p-4">
<table className="p-4">
<tbody className="items-center">
<tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center">
<td>
<UserIcon className="h-4 w-4" />
</td>
<td className="w-32">Username</td>
</div>
<td className="w-3/4 w-3/4 p-2 pl-0">
<input
type="text"
name="username"
value={tempRowContent.username}
onChange={handleTempRowContentChange}
onKeyDown={handleEnterPress}
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
/>
</td>
</tr>
<tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center">
<td>
<ListBulletIcon className="h-4 w-4" />
</td>
<td className="w-32">Role</td>
</div>
<td className="w-3/4 hover:bg-gray-50">
<TagsInput
presetValue={tempRowContent.role}
presetOptions={presetOptions}
setPresetOptions={setPresetOptions}
getTagColor={getTagColor}
setTagColors={setTagColors}
/>
</td>
</tr>
<tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center">
<td>
<EnvelopeIcon className="h-4 w-4" />
</td>
<td className="w-32">Email</td>
</div>
<td className="w-3/4 p-2 pl-0">
<input
type="text"
name="email"
value={tempRowContent.email}
onChange={handleTempRowContentChange}
onKeyDown={handleEnterPress}
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500"
/>
</td>
</tr>
<tr className="w-full text-xs items-center flex flex-row justify-between">
<div className="flex flex-row space-x-2 text-gray-500 items-center">
<td>
<ListBulletIcon className="h-4 w-4" />
</td>
<td className="w-32">Type of Program</td>
</div>
<td className="w-3/4 hover:bg-gray-50">
{/* {rowContent.program} */}
<TagsInput
presetValue={tempRowContent.program}
presetOptions={rolePresetOptions}
setPresetOptions={setRolePresetOptions}
getTagColor={getTagColor}
setTagColors={setTagColors}
/>
</td>
</tr>
</tbody>
</table>
<br />
</div>
</div>
</div>
);
};
export default Drawer;