Compare commits

..

No commits in common. "476016117bece69197fcddd0f186d952ba31f323" and "2766ad8eeea93c7fecff75970d0767db1fa1ff27" have entirely different histories.

13 changed files with 1170 additions and 362 deletions

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import UserTable from "@/components/Table/UserIndex"; import { Table } from "@/components/Table/Index";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
@ -38,7 +38,7 @@ export default function Page() {
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<PageLayout title="Users" icon={<UsersIcon />}> <PageLayout title="Users" icon={<UsersIcon />}>
<UserTable users={users} /> <Table users={users} />
</PageLayout> </PageLayout>
</div> </div>
); );

View File

@ -1,8 +1,8 @@
"use client"; "use client";
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import { ResourceTable } from "@/components/Table/ResourceIndex";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
import ResourceTable from "@/components/Table/ResourceIndex";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { BookmarkIcon } from "@heroicons/react/24/solid"; import { BookmarkIcon } from "@heroicons/react/24/solid";
@ -38,7 +38,7 @@ export default function Page() {
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<PageLayout title="Resources" icon={<BookmarkIcon />}> <PageLayout title="Resources" icon={<BookmarkIcon />}>
<ResourceTable resources={resources} /> <ResourceTable users={resources} />
</PageLayout> </PageLayout>
</div> </div>
); );

View File

@ -0,0 +1,92 @@
"use client";
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import User, { Role } from "@/utils/models/User";
import Loading from "@/components/auth/Loading";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const router = useRouter();
const [user, setUser] = useState<User>();
useEffect(() => {
async function getUser() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
console.log(data, error);
if (error) {
console.log("Accessed resource page but not logged in");
router.push("/auth/login");
return;
}
const userData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
);
const user: User = await userData.json();
setUser(user);
}
getUser();
}, [router]);
return (
<div className="flex-row">
{user ? (
<div>
{/* button to open sidebar */}
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"}
>
{
!isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" />
) // Icon for closing the sidebar
}
</button>
{/* sidebar */}
<div
className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen
? "translate-x-0"
: "-translate-x-full"
} w-64 transition duration-300 ease-in-out`}
>
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
</div>
{/* page ui */}
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"
}`}
>
{children}
</div>
</div>
) : (
<Loading />
)}
</div>
);
}

View File

@ -0,0 +1,177 @@
"use client";
import { PageLayout } from "@/components/PageLayout";
import { RowOpenAction } from "@/components/Table/RowOpenAction";
import { RowOptionMenu } from "@/components/Table/RowOptionMenu";
import { TestTable } from "@/components/Table/TestIndex";
import TagsInput from "@/components/TagsInput/Index";
import Resource from "@/utils/models/Resource";
import { createClient } from "@/utils/supabase/client";
import { Bars2Icon, BookmarkIcon } from "@heroicons/react/24/solid";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import { useEffect, useState } from "react";
export default function Page() {
const [resources, setResources] = useState<Resource[]>([]);
const columnHelper = createColumnHelper<Resource>();
useEffect(() => {
async function getResources() {
const supabase = createClient();
const { data, error } = await supabase.auth.getUser();
if (error) {
console.log("Accessed admin page but not logged in");
return;
}
const userListData = await fetch(
`${process.env.NEXT_PUBLIC_HOST}/api/resource/all?uuid=${data.user.id}`
);
const resourcesAPI: Resource[] = await userListData.json();
setResources(resourcesAPI);
}
getResources();
}, []);
const [data, setData] = useState<Resource[]>([]);
const deleteUser = (userId: number) => {
console.log(data);
setData((currentData) =>
currentData.filter((user) => user.id !== userId)
);
};
const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`);
setData((currentData) => {
const newData = currentData
.map((user) => {
if (user.id === userId) {
return { ...user, visible: !user.visible };
}
return user;
})
.sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
console.log(newData);
return newData;
});
};
const addUser = () => {
setData([...data]);
};
const [presetOptions, setPresetOptions] = useState([
"administrator",
"volunteer",
"employee",
]);
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);
};
const handleRowUpdate = (updatedRow: Resource) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) {
const updatedData = [...data];
updatedData[dataIndex] = updatedRow;
setData(updatedData);
}
};
const columns: ColumnDef<Resource, any>[] = [
columnHelper.display({
id: "options",
cell: (props) => (
<RowOptionMenu
onDelete={() => {}}
onHide={() => hideUser(props.row.original.id)}
/>
),
}),
columnHelper.accessor("name", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Name
</>
),
cell: (info) => (
<RowOpenAction
title={info.getValue()}
rowData={info.row.original}
onRowUpdate={handleRowUpdate}
/>
),
}),
columnHelper.accessor("link", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Link
</>
),
cell: (info) => (
<a
href={info.getValue()}
target={"_blank"}
className="ml-2 text-gray-500 underline hover:text-gray-400"
>
{info.getValue()}
</a>
),
}),
columnHelper.accessor("program", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Program
</>
),
cell: (info) => (
<TagsInput presetValue={info.getValue()} />
),
}),
columnHelper.accessor("summary", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Summary
</>
),
cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span>
),
}),
];
return (
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Resources" icon={<BookmarkIcon />}>
<TestTable initialData={resources} columns={columns}/>
</PageLayout>
</div>
);
}

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import ServiceTable from "@/components/Table/ServiceIndex"; import { ServiceTable } from "@/components/Table/ServiceIndex";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
@ -37,7 +37,7 @@ export default function Page() {
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<PageLayout title="Services" icon={<ClipboardIcon />}> <PageLayout title="Services" icon={<ClipboardIcon />}>
<ServiceTable services={services} /> <ServiceTable users={services} />
</PageLayout> </PageLayout>
</div> </div>
); );

View File

@ -0,0 +1,306 @@
// for showcasing to compass
import users from "./users.json";
import {
Cell,
ColumnDef,
Row,
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
sortingFns,
useReactTable,
} from "@tanstack/react-table";
import {
ChangeEvent,
useState,
useEffect,
FunctionComponent,
useRef,
ChangeEventHandler,
Key,
} from "react";
import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction";
import {
AtSymbolIcon,
Bars2Icon,
ArrowDownCircleIcon,
PlusIcon,
} from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils";
import User from "@/utils/models/User";
// For search
const fuzzyFilter = (
row: Row<any>,
columnId: string,
value: any,
addMeta: (meta: any) => void
) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value);
// Store the ranking info
addMeta(itemRank);
// Return if the item should be filtered in/out
return itemRank.passed;
};
export const Table = ({ users }: { users: User[] }) => {
const columnHelper = createColumnHelper<User>();
useEffect(() => {
const sortedUsers = [...users].sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
setData(sortedUsers);
}, [users]);
const deleteUser = (userId: number) => {
console.log(data);
setData((currentData) =>
currentData.filter((user) => user.id !== userId)
);
};
const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`);
setData((currentData) => {
const newData = currentData
.map((user) => {
if (user.id === userId) {
return { ...user, visible: !user.visible };
}
return user;
})
.sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
console.log(newData);
return newData;
});
};
const [presetOptions, setPresetOptions] = useState([
"administrator",
"volunteer",
"employee",
]);
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);
};
const columns = [
columnHelper.display({
id: "options",
cell: (props) => (
<RowOptionMenu
onDelete={() => deleteUser(props.row.original.id)}
onHide={() => hideUser(props.row.original.id)}
/>
),
}),
columnHelper.accessor("username", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Username
</>
),
cell: (info) => (
<RowOpenAction
title={info.getValue()}
rowData={info.row.original}
onRowUpdate={handleRowUpdate}
/>
),
}),
columnHelper.accessor("role", {
header: () => (
<>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Role
</>
),
cell: (info) => (
<TagsInput
presetValue={info.getValue()}
presetOptions={presetOptions}
setPresetOptions={setPresetOptions}
getTagColor={getTagColor}
setTagColors={setTagColors}
/>
),
}),
columnHelper.accessor("email", {
header: () => (
<>
<AtSymbolIcon className="inline align-top h-4" /> Email
</>
),
cell: (info) => (
<span className="ml-2 text-gray-500 underline hover:text-gray-400">
{info.getValue()}
</span>
),
}),
columnHelper.accessor("program", {
header: () => (
<>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Program
</>
),
cell: (info) => <TagsInput presetValue={info.getValue()} />,
}),
];
const [data, setData] = useState<User[]>([...users]);
const addUser = () => {
setData([...data]);
};
// Searching
const [query, setQuery] = useState("");
const handleSearchChange = (e: ChangeEvent) => {
const target = e.target as HTMLInputElement;
setQuery(String(target.value));
};
const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement;
console.log(key);
};
// TODO: Filtering
// TODO: Sorting
// added this fn for editing rows
const handleRowUpdate = (updatedRow: User) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) {
const updatedData = [...data];
updatedData[dataIndex] = updatedRow;
setData(updatedData);
}
};
const table = useReactTable({
columns,
data,
filterFns: {
fuzzy: fuzzyFilter,
},
state: {
globalFilter: query,
},
onGlobalFilterChange: setQuery,
globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(),
});
const handleRowData = (row: any) => {
const rowData: any = {};
row.cells.forEach((cell: any) => {
rowData[cell.column.id] = cell.value;
});
// Use rowData object containing data from all columns for the current row
console.log(rowData);
return rowData;
};
return (
<div className="flex flex-col">
<div className="flex flex-row justify-end">
<TableAction query={query} handleChange={handleSearchChange} />
</div>
<table className="w-full text-xs text-left rtl:text-right">
<thead className="text-xs text-gray-500 capitalize">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, i) => (
<th
scope="col"
className={
"p-2 border-gray-200 border-y font-medium " +
(1 < i && i < columns.length - 1
? "border-x"
: "")
}
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
// Individual row
const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`;
return (
<tr className={rowClassNames} key={row.id}>
{row.getVisibleCells().map((cell, i) => (
<td
key={cell.id}
className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
);
})}
</tbody>
<tfoot>
<tr>
<td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100}
onClick={addUser}
>
<span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" />
New
</span>
</td>
</tr>
</tfoot>
</table>
</div>
);
};

View File

@ -1,95 +1,305 @@
import { Bars2Icon } from "@heroicons/react/24/solid"; // for showcasing to compass
import { useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler"; import users from "./users.json";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import {
import { Table } from "@/components/Table/Table"; Cell,
import { RowOpenAction } from "@/components/Table/RowOpenAction"; ColumnDef,
import TagsInput from "@/components/TagsInput/Index"; Row,
import Resource from "@/utils/models/Resource"; createColumnHelper,
import { DataPoint } from "@/components/Table/Table"; flexRender,
getCoreRowModel,
/** getFilteredRowModel,
* Table componenet used for displaying resources sortingFns,
* @param props.resources List of resources to be displayed by the table useReactTable,
*/ } from "@tanstack/react-table";
export default function ResourceTable({ resources }: { resources: Resource[] }) { import {
const columnHelper = createColumnHelper<Resource>(); ChangeEvent,
const [data, setData] = useState<DataPoint[]>([...resources]); useState,
useEffect,
// TODO: Update preset options for resources FunctionComponent,
const { presetOptions, setPresetOptions, getTagColor } = useTagsHandler([ useRef,
"administrator", ChangeEventHandler,
"volunteer", Key,
"employee", } from "react";
]) import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction";
const handleRowUpdate = (updatedRow: DataPoint) => { import { TableAction } from "./TableAction";
const dataIndex = data.findIndex((row) => row.id === updatedRow.id); import {
if (dataIndex !== -1) { AtSymbolIcon,
const updatedData = [...data]; Bars2Icon,
updatedData[dataIndex] = updatedRow; ArrowDownCircleIcon,
setData(updatedData); PlusIcon,
} } from "@heroicons/react/24/solid";
}; import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils";
const columns: ColumnDef<Resource, any>[] = [ import Resource from "@/utils/models/Resource";
columnHelper.accessor("name", {
header: () => ( // For search
<> const fuzzyFilter = (
<Bars2Icon className="inline align-top h-4" /> Name row: Row<any>,
</> columnId: string,
), value: any,
cell: (info) => ( addMeta: (meta: any) => void
<RowOpenAction ) => {
title={info.getValue()} // Rank the item
rowData={info.row.original} const itemRank = rankItem(row.getValue(columnId), value);
onRowUpdate={handleRowUpdate}
/> // Store the ranking info
), addMeta(itemRank);
}),
columnHelper.accessor("link", { // Return if the item should be filtered in/out
header: () => ( return itemRank.passed;
<> };
<Bars2Icon className="inline align-top h-4" /> Link
</> // TODO: Rename everything to resources
), export const ResourceTable = ({ users }: { users: Resource[] }) => {
cell: (info) => ( const columnHelper = createColumnHelper<Resource>();
<a
href={info.getValue()} useEffect(() => {
target={"_blank"} const sortedUsers = [...users].sort((a, b) =>
className="ml-2 text-gray-500 underline hover:text-gray-400" a.visible === b.visible ? 0 : a.visible ? -1 : 1
> );
{info.getValue()} setData(sortedUsers);
</a> }, [users]);
),
}), const [presetOptions, setPresetOptions] = useState([
columnHelper.accessor("program", { "administrator",
header: () => ( "volunteer",
<> "employee",
<Bars2Icon className="inline align-top h-4" /> Program ]);
</>
), const deleteUser = (userId: number) => {
cell: (info) => ( console.log(data);
<TagsInput setData((currentData) =>
presetValue={info.getValue()} currentData.filter((user) => user.id !== userId)
presetOptions={presetOptions} );
setPresetOptions={setPresetOptions} };
getTagColor={getTagColor}
/> const hideUser = (userId: number) => {
), console.log(`Toggling visibility for user with ID: ${userId}`);
}), setData((currentData) => {
const newData = currentData
columnHelper.accessor("summary", { .map((user) => {
header: () => ( if (user.id === userId) {
<> return { ...user, visible: !user.visible };
<Bars2Icon className="inline align-top h-4" /> Summary }
</> return user;
), })
cell: (info) => ( .sort((a, b) =>
<span className="ml-2 text-gray-500">{info.getValue()}</span> a.visible === b.visible ? 0 : a.visible ? -1 : 1
), );
}),
]; console.log(newData);
return newData;
return <Table data={data} setData={setData} columns={columns}/> });
} };
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);
};
const columns = [
columnHelper.display({
id: "options",
cell: (props) => (
<RowOptionMenu
onDelete={() => {}}
onHide={() => hideUser(props.row.original.id)}
/>
),
}),
columnHelper.accessor("name", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Name
</>
),
cell: (info) => (
<RowOpenAction
title={info.getValue()}
rowData={info.row.original}
onRowUpdate={handleRowUpdate}
/>
),
}),
columnHelper.accessor("link", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Link
</>
),
cell: (info) => (
<a
href={info.getValue()}
target={"_blank"}
className="ml-2 text-gray-500 underline hover:text-gray-400"
>
{info.getValue()}
</a>
),
}),
columnHelper.accessor("program", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Program
</>
),
cell: (info) => <TagsInput presetValue={info.getValue()} />,
}),
columnHelper.accessor("summary", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Summary
</>
),
cell: (info) => (
<span className="ml-2 text-gray-500">{info.getValue()}</span>
),
}),
];
const [data, setData] = useState<Resource[]>([...users]);
const addUser = () => {
setData([...data]);
};
// Searching
const [query, setQuery] = useState("");
const handleSearchChange = (e: ChangeEvent) => {
const target = e.target as HTMLInputElement;
setQuery(String(target.value));
};
const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement;
console.log(key);
};
// TODO: Filtering
// TODO: Sorting
// added this fn for editing rows
const handleRowUpdate = (updatedRow: Resource) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) {
const updatedData = [...data];
updatedData[dataIndex] = updatedRow;
setData(updatedData);
}
};
const table = useReactTable({
columns,
data,
filterFns: {
fuzzy: fuzzyFilter,
},
state: {
globalFilter: query,
},
onGlobalFilterChange: setQuery,
globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(),
});
const handleRowData = (row: any) => {
const rowData: any = {};
row.cells.forEach((cell: any) => {
rowData[cell.column.id] = cell.value;
});
// Use rowData object containing data from all columns for the current row
console.log(rowData);
return rowData;
};
return (
<div className="flex flex-col">
<div className="flex flex-row justify-end">
<TableAction query={query} handleChange={handleSearchChange} />
</div>
<table className="w-full text-xs text-left rtl:text-right">
<thead className="text-xs text-gray-500 capitalize">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, i) => (
<th
scope="col"
className={
"p-2 border-gray-200 border-y font-medium " +
(1 < i && i < columns.length - 1
? "border-x"
: "")
}
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
// Individual row
const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`;
return (
<tr className={rowClassNames} key={row.id}>
{row.getVisibleCells().map((cell, i) => (
<td
key={cell.id}
className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
);
})}
</tbody>
<tfoot>
<tr>
<td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100}
onClick={addUser}
>
<span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" />
New
</span>
</td>
</tr>
</tfoot>
</table>
</div>
);
};

View File

@ -1,17 +1,10 @@
import Drawer from "@/components/Drawer/Drawer"; import Drawer from "@/components/Drawer/Drawer";
import { useState } from "react"; import { ChangeEvent, useState } from "react";
import { DataPoint } from "@/components/Table/Table";
type RowOpenActionProps = { export const RowOpenAction = ({ title, rowData, onRowUpdate }) => {
title: string,
rowData: DataPoint,
onRowUpdate: (updatedRow: DataPoint) => void;
}
export const RowOpenAction = ({ title, rowData, onRowUpdate }: RowOpenActionProps) => {
const [pageContent, setPageContent] = useState(""); const [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent: string) => { const handleDrawerContentChange = (newContent) => {
setPageContent(newContent); setPageContent(newContent);
}; };

View File

@ -1,38 +1,125 @@
import { Bars2Icon } from "@heroicons/react/24/solid"; // for showcasing to compass
import { useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import { Table } from "@/components/Table/Table";
import { RowOpenAction } from "@/components/Table/RowOpenAction";
import TagsInput from "@/components/TagsInput/Index";
import Service from "@/utils/models/Service";
import { DataPoint } from "@/components/Table/Table";
/** import users from "./users.json";
* Table componenet used for displaying services import {
* @param props.services List of services to be displayed by the table Cell,
*/ ColumnDef,
export default function ServiceTable({ services }: { services: Service[] }) { Row,
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
sortingFns,
useReactTable,
} from "@tanstack/react-table";
import {
ChangeEvent,
useState,
useEffect,
FunctionComponent,
useRef,
ChangeEventHandler,
Key,
} from "react";
import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction";
import {
AtSymbolIcon,
Bars2Icon,
ArrowDownCircleIcon,
PlusIcon,
} from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils";
import Service from "@/utils/models/Service";
// For search
const fuzzyFilter = (
row: Row<any>,
columnId: string,
value: any,
addMeta: (meta: any) => void
) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value);
// Store the ranking info
addMeta(itemRank);
// Return if the item should be filtered in/out
return itemRank.passed;
};
// TODO: Rename everything to service
export const ServiceTable = ({ users }: { users: Service[] }) => {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
const [data, setData] = useState<DataPoint[]>([...services]);
useEffect(() => {
// TODO: Update preset options for services const sortedUsers = [...users].sort((a, b) =>
const { presetOptions, setPresetOptions, getTagColor } = useTagsHandler([ a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
setData(sortedUsers);
}, [users]);
const deleteUser = (userId: number) => {
console.log(data);
setData((currentData) =>
currentData.filter((user) => user.id !== userId)
);
};
const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`);
setData((currentData) => {
const newData = currentData
.map((user) => {
if (user.id === userId) {
return { ...user, visible: !user.visible };
}
return user;
})
.sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
console.log(newData);
return newData;
});
};
const [presetOptions, setPresetOptions] = useState([
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]) ]);
const [tagColors, setTagColors] = useState(new Map());
const handleRowUpdate = (updatedRow: DataPoint) => { const getTagColor = (tag: string) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id); if (!tagColors.has(tag)) {
if (dataIndex !== -1) { const colors = [
const updatedData = [...data]; "bg-cyan-100",
updatedData[dataIndex] = updatedRow; "bg-blue-100",
setData(updatedData); "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);
}; };
const columns: ColumnDef<Service, any>[] = [ const columns = [
columnHelper.display({
id: "options",
cell: (props) => (
<RowOptionMenu
onDelete={() => {}}
onHide={() => hideUser(props.row.original.id)}
/>
),
}),
columnHelper.accessor("name", { columnHelper.accessor("name", {
header: () => ( header: () => (
<> <>
@ -63,14 +150,7 @@ export default function ServiceTable({ services }: { services: Service[] }) {
<Bars2Icon className="inline align-top h-4" /> Program <Bars2Icon className="inline align-top h-4" /> Program
</> </>
), ),
cell: (info) => ( cell: (info) => <TagsInput presetValue={info.getValue()} />,
<TagsInput
presetValue={info.getValue()}
presetOptions={presetOptions}
setPresetOptions={setPresetOptions}
getTagColor={getTagColor}
/>
),
}), }),
columnHelper.accessor("requirements", { columnHelper.accessor("requirements", {
header: () => ( header: () => (
@ -79,12 +159,10 @@ export default function ServiceTable({ services }: { services: Service[] }) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
// TODO: Setup different tag handler for requirements
<TagsInput <TagsInput
presetValue={info.getValue()[0] !== "" ? info.getValue() : ["N/A"]} presetValue={
presetOptions={presetOptions} info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
setPresetOptions={setPresetOptions} }
getTagColor={getTagColor}
/> />
), ),
}), }),
@ -101,5 +179,134 @@ export default function ServiceTable({ services }: { services: Service[] }) {
}), }),
]; ];
return <Table data={data} setData={setData} columns={columns} /> const [data, setData] = useState<Service[]>([...users]);
const addUser = () => {
setData([...data]);
};
// Searching
const [query, setQuery] = useState("");
const handleSearchChange = (e: ChangeEvent) => {
const target = e.target as HTMLInputElement;
setQuery(String(target.value));
};
const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement;
console.log(key);
};
// TODO: Filtering
// TODO: Sorting
// added this fn for editing rows
const handleRowUpdate = (updatedRow: Service) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) {
const updatedData = [...data];
updatedData[dataIndex] = updatedRow;
setData(updatedData);
}
};
const table = useReactTable({
columns,
data,
filterFns: {
fuzzy: fuzzyFilter,
},
state: {
globalFilter: query,
},
onGlobalFilterChange: setQuery,
globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(),
});
const handleRowData = (row: any) => {
const rowData: any = {};
row.cells.forEach((cell: any) => {
rowData[cell.column.id] = cell.value;
});
// Use rowData object containing data from all columns for the current row
console.log(rowData);
return rowData;
};
return (
<div className="flex flex-col">
<div className="flex flex-row justify-end">
<TableAction query={query} handleChange={handleSearchChange} />
</div>
<table className="w-full text-xs text-left rtl:text-right">
<thead className="text-xs text-gray-500 capitalize">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, i) => (
<th
scope="col"
className={
"p-2 border-gray-200 border-y font-medium " +
(1 < i && i < columns.length - 1
? "border-x"
: "")
}
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
// Individual row
const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`;
return (
<tr className={rowClassNames} key={row.id}>
{row.getVisibleCells().map((cell, i) => (
<td
key={cell.id}
className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
);
})}
</tbody>
<tfoot>
<tr>
<td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100}
onClick={addUser}
>
<span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" />
New
</span>
</td>
</tr>
</tfoot>
</table>
</div>
);
}; };

View File

@ -1,34 +1,36 @@
import { // for showcasing to compass
Row, import {
Cell,
ColumnDef, ColumnDef,
useReactTable, Row,
getCoreRowModel, createColumnHelper,
flexRender, flexRender,
createColumnHelper getCoreRowModel,
} from "@tanstack/react-table"; getFilteredRowModel,
sortingFns,
useReactTable,
} from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
FunctionComponent,
useRef,
ChangeEventHandler,
Key, Key,
Dispatch,
SetStateAction
} from "react"; } from "react";
import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { PlusIcon } from "@heroicons/react/24/solid"; import {
AtSymbolIcon,
Bars2Icon,
ArrowDownCircleIcon,
PlusIcon,
} from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils"; import { rankItem } from "@tanstack/match-sorter-utils";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
import Service from "@/utils/models/Service";
import User from "@/utils/models/User";
import { RowOptionMenu } from "./RowOptionMenu";
export type DataPoint = Resource | User | Service;
type TableProps = {
data: DataPoint[],
setData: Dispatch<SetStateAction<DataPoint[]>>
columns: ColumnDef<any, any>[]
};
// For search // For search
const fuzzyFilter = ( const fuzzyFilter = (
@ -47,66 +49,16 @@ const fuzzyFilter = (
return itemRank.passed; return itemRank.passed;
}; };
/** // TODO: Rename everything to resources
* General componenet that holds shared functionality for any data table component export const TestTable = ({ initialData, columns }: { initialData: Resource[], columns: ColumnDef<any, any>[] }) => {
* @param props.data List of data, managed through state, to be held in the table
* @param props.setData State setter to be used for data manipulation methods
* @param props.columns Column definitions made with Tanstack columnHelper
*/
export const Table = ({ data, setData, columns }: TableProps) => {
const columnHelper = createColumnHelper<Resource>();
useEffect(() => { useEffect(() => {
const sortedData = [...data].sort((a, b) => const sortedData = [...initialData].sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1 a.visible === b.visible ? 0 : a.visible ? -1 : 1
); );
setData(sortedData); setData(sortedData);
}, [data, setData]); }, [initialData]);
const [data, setData] = useState<Resource[]>([...initialData]);
// Data manipulation
// TODO: Connect data manipulation methods to the database (deleteResource, hideResource, addResource)
const deleteData = (dataId: number) => {
console.log(data);
setData((currentData) =>
currentData.filter((data) => data.id !== dataId)
);
};
const hideData = (dataId: number) => {
console.log(`Toggling visibility for data with ID: ${dataId}`);
setData((currentData) => {
const newData = currentData
.map((data) => {
if (data.id === dataId) {
return { ...data, visible: !data.visible };
}
return data;
})
.sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
console.log(newData);
return newData;
});
};
const addData = () => {
setData([...data]);
};
// Add data manipulation options to the first column
columns.unshift(
columnHelper.display({
id: "options",
cell: (props) => (
<RowOptionMenu
onDelete={() => deleteData(props.row.original.id)}
onHide={() => hideData(props.row.original.id)}
/>
),
})
)
// Searching // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -124,6 +76,9 @@ export const Table = ({ data, setData, columns }: TableProps) => {
// TODO: Sorting // TODO: Sorting
// added this fn for editing rows
const table = useReactTable({ const table = useReactTable({
columns, columns,
data, data,
@ -182,9 +137,9 @@ export const Table = ({ data, setData, columns }: TableProps) => {
<tbody> <tbody>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
// Individual row // Individual row
const isDataVisible = row.original.visible; const isUserVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isDataVisible ? "bg-gray-200 text-gray-500" : "" !isUserVisible ? "bg-gray-200 text-gray-500" : ""
}`; }`;
return ( return (
<tr className={rowClassNames} key={row.id}> <tr className={rowClassNames} key={row.id}>
@ -210,7 +165,7 @@ export const Table = ({ data, setData, columns }: TableProps) => {
<td <td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50" className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100} colSpan={100}
onClick={addData} onClick={addUser}
> >
<span className="flex ml-1 text-gray-500"> <span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" /> <PlusIcon className="inline h-4 mr-1" />

View File

@ -1,97 +0,0 @@
import { ArrowDownCircleIcon, AtSymbolIcon, Bars2Icon } from "@heroicons/react/24/solid";
import { useState } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import { Table } from "@/components/Table/Table";
import { RowOpenAction } from "@/components/Table/RowOpenAction";
import TagsInput from "@/components/TagsInput/Index";
import User from "@/utils/models/User";
import { DataPoint } from "@/components/Table/Table";
/**
* Table componenet used for displaying users
* @param props.users List of users to be displayed by the table
*/
export default function UserTable({ users }: { users: User[] }) {
const columnHelper = createColumnHelper<User>();
const [data, setData] = useState<DataPoint[]>([...users]);
const { presetOptions, setPresetOptions, getTagColor } = useTagsHandler([
"administrator",
"volunteer",
"employee",
])
const handleRowUpdate = (updatedRow: DataPoint) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) {
const updatedData = [...data];
updatedData[dataIndex] = updatedRow;
setData(updatedData);
}
};
const columns: ColumnDef<User, any>[] = [
columnHelper.accessor("username", {
header: () => (
<>
<Bars2Icon className="inline align-top h-4" /> Username
</>
),
cell: (info) => (
<RowOpenAction
title={info.getValue()}
rowData={info.row.original}
onRowUpdate={handleRowUpdate}
/>
),
}),
columnHelper.accessor("role", {
header: () => (
<>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Role
</>
),
cell: (info) => (
<TagsInput
presetValue={info.getValue()}
presetOptions={presetOptions}
setPresetOptions={setPresetOptions}
getTagColor={getTagColor}
/>
),
}),
columnHelper.accessor("email", {
header: () => (
<>
<AtSymbolIcon className="inline align-top h-4" /> Email
</>
),
cell: (info) => (
<span className="ml-2 text-gray-500 underline hover:text-gray-400">
{info.getValue()}
</span>
),
}),
columnHelper.accessor("program", {
header: () => (
<>
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
Program
</>
),
// TODO: Setup different tags handler for program
cell: (info) => (
<TagsInput
presetValue={info.getValue()}
presetOptions={presetOptions}
setPresetOptions={setPresetOptions}
getTagColor={getTagColor}
/>
),
}),
];
return <Table data={data} setData={setData} columns={columns}/>
}

View File

@ -7,7 +7,7 @@ import { CreateNewTagAction } from "./CreateNewTagAction";
interface TagsInputProps { interface TagsInputProps {
presetOptions: string[]; presetOptions: string[];
presetValue: string | string[]; presetValue: string | string[];
setPresetOptions: Dispatch<SetStateAction<string[]>>; setPresetOptions: Dispatch<SetStateAction<string | string[]>>;
getTagColor(tag: string): string; getTagColor(tag: string): string;
} }

View File

@ -1,35 +0,0 @@
import { useState } from 'react';
/**
* Custom hook used to handle the state of tag options and colors
* @param initialOptions Initial value for preset options
* @returns An object with three fields intended to be passed into a `TagsInput` component:
* - `presetOptions` - the current state of tag options
* - `setPresetOptions` - the state setter for presetOptions
* - `getTagColor` - function that retrieves the color for the given tag
*/
export default function useTagsHandler(initialOptions: string[]) {
const [presetOptions, setPresetOptions] = useState(initialOptions);
const [tagColors, setTagColors] = useState(new Map<string, string>());
const getTagColor = (tag: string): 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 randomColor;
}
// Since we populate any missing keys, .get will never return undefined,
// so we are safe to typecast to prevent a type error
return tagColors.get(tag) as string;
};
return { presetOptions, setPresetOptions, getTagColor }
}