Compare commits

..

No commits in common. "a516c414f64a4c263490dc761a68848fce4c02e2" and "00ba6d7df1484b4a0af8f222ce19dd8f0b21e6d1" have entirely different histories.

16 changed files with 519 additions and 469 deletions

View File

@ -44,7 +44,7 @@ RUN mkdir -p /etc/apt/keyrings \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list \ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \ && apt-get update \
&& apt-get install nodejs -y \ && apt-get install nodejs -y \
&& npm install -g npm@10.8.2 \ && npm install -g npm@latest \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user # Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user

View File

@ -77,8 +77,6 @@ export default function Page() {
alt="Compass Center logo." alt="Compass Center logo."
width={100} width={100}
height={91} height={91}
style={{ height: "auto", width: "auto" }}
priority
/> />
<h1 className="font-bold text-2xl text-purple-800">Login</h1> <h1 className="font-bold text-2xl text-purple-800">Login</h1>
<div className="mb-6"> <div className="mb-6">

View File

@ -76,8 +76,6 @@ export default function Page() {
alt="Compass Center logo." alt="Compass Center logo."
width={100} width={100}
height={91} height={91}
style={{ height: "auto", width: "auto" }}
priority
/> />
<h1 className="font-bold text-2xl text-purple-800">Login</h1> <h1 className="font-bold text-2xl text-purple-800">Login</h1>
<div className="mb-6"> <div className="mb-6">

View File

@ -14,56 +14,61 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
type InputType =
| "text"
| "email"
| "textarea"
| "select-one"
| "select-multiple";
export interface Details {
key: string;
label: string;
inputType: InputType;
icon: ReactNode;
presetOptionsValues?: string[];
presetOptionsSetter?: Dispatch<SetStateAction<string[]>>;
}
type DrawerProps = { type DrawerProps = {
titleKey: string; title: string;
details: Details[]; 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; rowContent?: any;
setRowContent?: Dispatch<SetStateAction<any>>; setData: Dispatch<SetStateAction<any>>;
}; };
interface EditContent {
content: string;
isEditing: boolean;
}
const Drawer: FunctionComponent<DrawerProps> = ({ const Drawer: FunctionComponent<DrawerProps> = ({
titleKey, title,
details, children,
onSave,
editableContent,
rowContent, rowContent,
setRowContent, setData,
}: 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 = ( const handleTempRowContentChange = (e) => {
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target; const { name, value } = e.target;
setTempRowContent((prev: any) => ({ console.log(name);
...prev, console.log(value);
setTempRowContent((prevContent) => ({
...prevContent,
[name]: value, [name]: value,
})); }));
}; };
const handleEnterPress = ( const handleEnterPress = (e) => {
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);
}
} }
}; };
@ -94,6 +99,34 @@ 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
@ -104,6 +137,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
> >
Open Open
</button> </button>
<div className={drawerClassName}></div>
<div className={drawerClassName}> <div className={drawerClassName}>
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<div className="flex flex-row items-center justify-between space-x-2"> <div className="flex flex-row items-center justify-between space-x-2">
@ -111,7 +145,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[titleKey]} {rowContent.username}
</h2> </h2>
</div> </div>
<div> <div>
@ -136,116 +170,81 @@ const Drawer: FunctionComponent<DrawerProps> = ({
</div> </div>
</div> </div>
<div className="p-4"> <div className="p-4">
<div className="flex flex-col space-y-3"> <table className="p-4">
{details.map((detail, index) => { <tbody className="items-center">
const value = tempRowContent[detail.key]; <tr className="w-full text-xs items-center flex flex-row justify-between">
let valueToRender = <></>; <div className="flex flex-row space-x-2 text-gray-500 items-center">
<td>
switch (detail.inputType) { <UserIcon className="h-4 w-4" />
case "select-one": </td>
case "select-multiple": <td className="w-32">Username</td>
valueToRender = (
<div className="flex-1">
<div className="hover:bg-gray-50 rounded-md px-2 py-1">
<TagsInput
presetValue={
typeof value ===
"string"
? [value]
: value
}
presetOptions={
detail.presetOptionsValues ||
[]
}
setPresetOptions={
detail.presetOptionsSetter ||
(() => {})
}
/>
</div>
</div>
);
break;
case "textarea":
valueToRender = (
<div className="flex-1">
<div className="hover:bg-gray-50 rounded-md px-2 py-1">
<textarea
name={detail.key}
value={value}
onChange={
handleTempRowContentChange
}
onKeyDown={handleEnterPress}
rows={4}
onInput={(e) => {
const target =
e.target as HTMLTextAreaElement;
target.style.height =
"auto";
target.style.height =
target.scrollHeight +
"px";
}}
className="w-full p-2 focus:outline-none border border-gray-200 rounded-md resize-none font-normal bg-transparent"
/>
</div>
</div>
);
break;
case "text":
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 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>
); <td className="w-3/4 w-3/4 p-2 pl-0">
})} <input
</div> 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 /> <br />
</div> </div>
</div> </div>

View File

@ -1,10 +1,4 @@
import { import { Bars2Icon } from "@heroicons/react/24/solid";
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";
@ -12,8 +6,6 @@ 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[];
@ -28,101 +20,60 @@ type ResourceTableProps = {
export default function ResourceTable({ data, setData }: ResourceTableProps) { export default function ResourceTable({ data, setData }: ResourceTableProps) {
const columnHelper = createColumnHelper<Resource>(); const columnHelper = createColumnHelper<Resource>();
const [programPresets, setProgramPresets] = useState([ // Set up tag handling
"domestic", const programProps = useTagsHandler(["community", "domestic", "economic"]);
"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: () => (
<> <>
<UserIcon className="inline align-top h-4" /> Name <Bars2Icon 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: () => (
<> <>
<LinkIcon className="inline align-top h-4" /> Link <Bars2Icon className="inline align-top h-4" /> Link
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex items-start gap-2 px-2"> <a
<a href={info.getValue()}
href={info.getValue()} target={"_blank"}
target="_blank" className="ml-2 text-gray-500 underline hover:text-gray-400"
className="text-gray-500 underline hover:text-gray-400 break-all" >
> {info.getValue()}
{info.getValue()} </a>
</a>
</div>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<ListBulletIcon className="inline align-top h-4" /> Program <Bars2Icon className="inline align-top h-4" /> Program
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex flex-wrap gap-2 items-center px-2"> <TagsInput presetValue={info.getValue()} {...programProps} />
<Tag>{info.getValue()}</Tag>
</div>
), ),
}), }),
columnHelper.accessor("summary", { columnHelper.accessor("summary", {
header: () => ( header: () => (
<> <>
<DocumentTextIcon className="inline align-top h-4" />{" "} <Bars2Icon className="inline align-top h-4" /> Summary
Summary
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex items-start gap-2 px-2 py-1"> <span className="ml-2 text-gray-500">{info.getValue()}</span>
<span className="text-gray-500 max-h-8 overflow-y-auto">
{info.getValue()}
</span>
</div>
), ),
}), }),
]; ];

View File

@ -1,37 +1,37 @@
import Drawer, { Details } from "@/components/Drawer/Drawer"; import Drawer 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
titleKey={titleKey} title="My Drawer Title"
editableContent={pageContent}
rowContent={rowData} rowContent={rowData}
details={details} onSave={handleDrawerContentChange}
setRowContent={setData} setData={setData}
/> >
{pageContent}
</Drawer>
</span> </span>
</div> </div>
); );

View File

@ -1,17 +1,11 @@
import { import { Bars2Icon } from "@heroicons/react/24/solid";
Bars2Icon, import { Dispatch, SetStateAction } from "react";
CheckCircleIcon, import useTagsHandler from "@/components/TagsInput/TagsHandler";
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[];
@ -26,13 +20,11 @@ type ServiceTableProps = {
export default function ServiceTable({ data, setData }: ServiceTableProps) { export default function ServiceTable({ data, setData }: ServiceTableProps) {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
const [programPresets, setProgramPresets] = useState([ // Set up tag handling
"domestic", const programProps = useTagsHandler(["community", "domestic", "economic"]);
"community",
"economic",
]);
const [requirementPresets, setRequirementPresets] = useState([ // TODO: Dynamically or statically get full list of preset requirement tag options
const requirementProps = useTagsHandler([
"anonymous", "anonymous",
"confidential", "confidential",
"referral required", "referral required",
@ -42,111 +34,67 @@ 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: () => (
<> <>
<UserIcon className="inline align-top h-4" /> Name <Bars2Icon 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: () => (
<> <>
<CheckCircleIcon className="inline align-top h-4" /> Status <Bars2Icon className="inline align-top h-4" /> Status
</> </>
), ),
cell: (info) => ( cell: (info) => (
<span className="text-gray-500 px-2">{info.getValue()}</span> <span className="ml-2 text-gray-500">{info.getValue()}</span>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<ListBulletIcon className="inline align-top h-4" /> Program <Bars2Icon className="inline align-top h-4" /> Program
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex flex-wrap gap-2 items-center px-2"> <TagsInput presetValue={info.getValue()} {...programProps} />
<Tag>{info.getValue()}</Tag>
</div>
), ),
}), }),
columnHelper.accessor("requirements", { columnHelper.accessor("requirements", {
header: () => ( header: () => (
<> <>
<ListBulletIcon className="inline align-top h-4" />{" "} <Bars2Icon className="inline align-top h-4" /> Requirements
Requirements
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex flex-wrap gap-2 items-center px-2"> // TODO: Setup different tag handler for requirements
{info.getValue().map((tag: string, index: number) => { <TagsInput
return <Tag key={index}>{tag}</Tag>; presetValue={
})} info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
</div> }
{...requirementProps}
/>
), ),
}), }),
columnHelper.accessor("summary", { columnHelper.accessor("summary", {
header: () => ( header: () => (
<> <>
<DocumentTextIcon className="inline align-top h-4" />{" "} <Bars2Icon className="inline align-top h-4" /> Summary
Summary
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex items-start gap-2 px-2 py-1"> <span className="ml-2 text-gray-500">{info.getValue()}</span>
<span className="text-gray-500 max-h-8 overflow-y-auto">
{info.getValue()}
</span>
</div>
), ),
}), }),
]; ];

View File

@ -2,19 +2,14 @@ import {
ArrowDownCircleIcon, ArrowDownCircleIcon,
AtSymbolIcon, AtSymbolIcon,
Bars2Icon, Bars2Icon,
EnvelopeIcon,
ListBulletIcon,
UserIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction } 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[];
@ -29,83 +24,46 @@ type UserTableProps = {
export default function UserTable({ data, setData }: UserTableProps) { export default function UserTable({ data, setData }: UserTableProps) {
const columnHelper = createColumnHelper<User>(); const columnHelper = createColumnHelper<User>();
const [rolePresets, setRolePresets] = useState([ // Set up tag handling
"admin", const roleProps = useTagsHandler([
"administrator",
"volunteer", "volunteer",
"employee", "employee",
]); ]);
const [programPresets, setProgramPresets] = useState([ const programProps = useTagsHandler(["community", "domestic", "economic"]);
"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: () => (
<> <>
<UserIcon className="inline align-top h-4" /> Username <Bars2Icon 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: () => (
<> <>
<ListBulletIcon className="inline align-top h-4" /> Role <ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Role
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex ml-2 flex-wrap gap-2 items-center"> <TagsInput presetValue={info.getValue()} {...roleProps} />
<Tag>{info.getValue()}</Tag>
</div>
), ),
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
header: () => ( header: () => (
<> <>
<EnvelopeIcon className="inline align-top h-4" /> Email <AtSymbolIcon className="inline align-top h-4" /> Email
</> </>
), ),
cell: (info) => ( cell: (info) => (
@ -117,15 +75,12 @@ export default function UserTable({ data, setData }: UserTableProps) {
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
<ListBulletIcon className="inline align-top h-4" /> Program <ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Program
</> </>
), ),
cell: (info) => ( cell: (info) => (
<div className="flex ml-2 flex-wrap gap-2 items-center"> <TagsInput presetValue={info.getValue()} {...programProps} />
{info.getValue().map((tag: string, index: number) => {
return <Tag key={index}>{tag}</Tag>;
})}
</div>
), ),
}), }),
]; ];

View File

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

View File

@ -1,21 +1,11 @@
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid"; import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
import { useState } from "react"; import { useState } from "react";
interface DropdownActionProps { export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => {
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: React.KeyboardEvent<HTMLInputElement>) => { const editTagOption = (e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleEditTag(tag, inputValue); handleEditTag(tag, inputValue);
setVisible(false); setVisible(false);

View File

@ -6,53 +6,51 @@ import { CreateNewTagAction } from "./CreateNewTagAction";
interface TagsInputProps { interface TagsInputProps {
presetOptions: string[]; presetOptions: string[];
presetValue: string[]; presetValue: string | 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>>(
new Set(presetValue.map((tag) => tag.toLowerCase())) typeof presetValue === "string"
); ? new Set([presetValue])
const [options, setOptions] = useState<Set<string>>( : new Set(presetValue)
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>) => {
setFilteredOptions(() => { setOptions(() => {
const newOptions = presetOptions.filter((item) => const newOptions = presetOptions.filter((item) =>
item.includes(e.target.value.toLowerCase()) item.includes(e.target.value.toLowerCase())
); );
@ -63,45 +61,39 @@ 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()) {
addTag(e); // setPresetOptions((prevPreset) => {
// 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)) {
setTags(new Set(Array.from(tags).concat(tagToAdd))); // Corrected syntax for checking if a Set contains an item
setTags((prevTags) => new Set(prevTags).add(tagToAdd));
} }
}; };
const handleDeleteTag = (tagToDelete: string) => { const handleDeleteTag = (tagToDelete: string) => {
setTags(new Set(Array.from(tags).filter((tag) => tag !== tagToDelete))); setTags((prevTags) => {
const updatedTags = new Set(prevTags);
updatedTags.delete(tagToDelete);
return updatedTags;
});
}; };
const handleDeleteTagOption = (tagToDelete: string) => { const handleDeleteTagOption = (tagToDelete: string) => {
setOptions( // setPresetOptions(presetOptions.filter(tag => tag !== tagToDelete));
new Set( setOptions((prevOptions) => {
Array.from(options).filter((option) => option !== tagToDelete) const updatedOptions = new Set(prevOptions);
) 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);
} }
@ -109,20 +101,23 @@ const TagsInput: React.FC<TagsInputProps> = ({
const handleEditTag = (oldTag: string, newTag: string) => { const handleEditTag = (oldTag: string, newTag: string) => {
if (oldTag !== newTag) { if (oldTag !== newTag) {
setTags( setTags((prevTags) => {
new Set( const tagsArray = Array.from(prevTags);
Array.from(tags) const oldTagIndex = tagsArray.indexOf(oldTag);
.filter((tag) => tag !== oldTag) if (oldTagIndex !== -1) {
.concat(newTag) tagsArray.splice(oldTagIndex, 1, newTag);
) }
); return new Set(tagsArray);
setOptions( });
new Set(
Array.from(options) setOptions((prevOptions) => {
.filter((option) => option !== oldTag) const optionsArray = Array.from(prevOptions);
.concat(newTag) const oldTagIndex = optionsArray.indexOf(oldTag);
) if (oldTagIndex !== -1) {
); optionsArray.splice(oldTagIndex, 1, newTag);
}
return new Set(optionsArray);
});
} }
}; };
@ -162,13 +157,10 @@ const TagsInput: React.FC<TagsInputProps> = ({
handleDeleteTag={handleDeleteTagOption} handleDeleteTag={handleDeleteTagOption}
handleEditTag={handleEditTag} handleEditTag={handleEditTag}
handleAdd={handleSelectTag} handleAdd={handleSelectTag}
tags={filteredOptions} tags={options}
/> />
{inputValue.length > 0 && ( {inputValue.length > 0 && (
<CreateNewTagAction <CreateNewTagAction input={inputValue} />
input={inputValue}
addTag={addTag}
/>
)} )}
</div> </div>
</div> </div>

View File

@ -1,25 +1,14 @@
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";
interface TagProps { export const Tag = ({ children, handleDelete, active = false }) => {
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 <button onClick={() => handleDelete(children)}>
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,19 +1,12 @@
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

@ -11,8 +11,6 @@ const Loading = () => {
alt="Compass Center logo." alt="Compass Center logo."
width={100} width={100}
height={91} height={91}
style={{ height: "auto", width: "auto" }}
priority
/> />
<h1 className={styles.loadingTitle}>Loading...</h1> <h1 className={styles.loadingTitle}>Loading...</h1>
<div className={styles.loadingSpinner}></div> <div className={styles.loadingSpinner}></div>

View File

@ -11,8 +11,6 @@ const LoggingOut = () => {
alt="Compass Center logo." alt="Compass Center logo."
width={100} width={100}
height={91} height={91}
style={{ height: "auto", width: "auto" }}
priority
/> />
<h1 className={styles.loadingTitle}>Signing out...</h1> <h1 className={styles.loadingTitle}>Signing out...</h1>
<div className={styles.loadingSpinner}></div> <div className={styles.loadingSpinner}></div>

View File

@ -0,0 +1,247 @@
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;