diff --git a/zettelkasten-front/package-lock.json b/zettelkasten-front/package-lock.json
index bec015d..377a7ca 100644
--- a/zettelkasten-front/package-lock.json
+++ b/zettelkasten-front/package-lock.json
@@ -9,6 +9,8 @@
"version": "0.1.0",
"dependencies": {
"@headlessui/react": "^2.2.0",
+ "@tanstack/match-sorter-utils": "^8.19.4",
+ "@tanstack/react-table": "^8.20.6",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -1418,6 +1420,42 @@
"node": ">=4"
}
},
+ "node_modules/@tanstack/match-sorter-utils": {
+ "version": "8.19.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
+ "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
+ "license": "MIT",
+ "dependencies": {
+ "remove-accents": "0.5.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-table": {
+ "version": "8.20.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",
+ "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/table-core": "8.20.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/@tanstack/react-virtual": {
"version": "3.10.9",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz",
@@ -1435,6 +1473,19 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@tanstack/table-core": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
+ "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tanstack/virtual-core": {
"version": "3.10.9",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz",
@@ -5301,6 +5352,12 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remove-accents": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
+ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.6",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
diff --git a/zettelkasten-front/package.json b/zettelkasten-front/package.json
index 68dde92..07b8b39 100644
--- a/zettelkasten-front/package.json
+++ b/zettelkasten-front/package.json
@@ -4,6 +4,8 @@
"private": true,
"dependencies": {
"@headlessui/react": "^2.2.0",
+ "@tanstack/match-sorter-utils": "^8.19.4",
+ "@tanstack/react-table": "^8.20.6",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
diff --git a/zettelkasten-front/src/components/users/UserListItem.tsx b/zettelkasten-front/src/components/users/UserListItem.tsx
index 37ef4fe..6b8a809 100644
--- a/zettelkasten-front/src/components/users/UserListItem.tsx
+++ b/zettelkasten-front/src/components/users/UserListItem.tsx
@@ -1,6 +1,5 @@
-import React, { useState, useEffect } from "react";
+import React from "react";
import { Link } from "react-router-dom";
-
import { User } from "../../models/User";
interface UserListItemProps {
@@ -9,17 +8,40 @@ interface UserListItemProps {
export function UserListItem({ user }: UserListItemProps) {
return (
-
- {user["id"]} |
-
- {user.username}
+ |
+ {user.id} |
+
+
+ {user.username}
+
+ |
+
+
+ {user.is_admin ? "Yes" : "No"}
+
+ |
+ {new Date(user.last_login).toLocaleString()} |
+ {user.email} |
+
+
+ {user.email_validated ? "Verified" : "Pending"}
+
+ |
+
+
+ {user.stripe_subscription_status}
+
|
- {user["is_admin"] ? "Yes" : "No"} |
- {user["last_login"]} |
- {user["email"]} |
- {user["email_validated"] ? "Yes" : "No"} |
- {user["stripe_subscription_status"]} |
- {user["card_count"]} |
+ {new Date(user.created_at).toLocaleString()} |
+ {user.card_count} |
);
}
diff --git a/zettelkasten-front/src/pages/admin/AdminMailingListPage.tsx b/zettelkasten-front/src/pages/admin/AdminMailingListPage.tsx
index e517542..bf128d8 100644
--- a/zettelkasten-front/src/pages/admin/AdminMailingListPage.tsx
+++ b/zettelkasten-front/src/pages/admin/AdminMailingListPage.tsx
@@ -1,8 +1,28 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
import { getMailingListSubscribers, MailingListSubscriber } from "../../api/users";
+import {
+ useReactTable,
+ getCoreRowModel,
+ getSortedRowModel,
+ getFilteredRowModel,
+ flexRender,
+ createColumnHelper,
+ FilterFn,
+ SortingState,
+ ColumnDef,
+} from "@tanstack/react-table";
+import { rankItem } from "@tanstack/match-sorter-utils";
+
+const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => {
+ const itemRank = rankItem(row.getValue(columnId), value);
+ addMeta({ itemRank });
+ return itemRank.passed;
+};
export function AdminMailingListPage() {
const [subscribers, setSubscribers] = useState([]);
+ const [sorting, setSorting] = useState([]);
+ const [globalFilter, setGlobalFilter] = useState("");
useEffect(() => {
const fetchSubscribers = async () => {
@@ -16,54 +36,117 @@ export function AdminMailingListPage() {
fetchSubscribers();
}, []);
+ const columnHelper = createColumnHelper();
+
+ const columns = useMemo[]>(
+ () => [
+ columnHelper.accessor("id", {
+ header: "ID",
+ cell: (info) => info.getValue(),
+ }),
+ columnHelper.accessor("email", {
+ header: "Email",
+ cell: (info) => info.getValue(),
+ }),
+ columnHelper.accessor("subscribed", {
+ header: "Status",
+ cell: (info) => (
+
+ {info.getValue() ? "Subscribed" : "Unsubscribed"}
+
+ ),
+ }),
+ columnHelper.accessor("welcome_email_sent", {
+ header: "Welcome Email",
+ cell: (info) => (
+
+ {info.getValue() ? "Sent" : "Pending"}
+
+ ),
+ }),
+ columnHelper.accessor("created_at", {
+ header: "Created At",
+ cell: (info) => new Date(info.getValue()).toLocaleString(),
+ }),
+ columnHelper.accessor("updated_at", {
+ header: "Updated At",
+ cell: (info) => new Date(info.getValue()).toLocaleString(),
+ }),
+ ],
+ []
+ );
+
+ const table = useReactTable({
+ data: subscribers,
+ columns,
+ state: {
+ sorting,
+ globalFilter,
+ },
+ onSortingChange: setSorting,
+ onGlobalFilterChange: setGlobalFilter,
+ globalFilterFn: fuzzyFilter,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ });
+
return (
-
Mailing List Subscribers
+
+
Mailing List Subscribers
+ setGlobalFilter(e.target.value)}
+ className="px-4 py-2 border rounded-lg"
+ placeholder="Search all columns..."
+ />
+
-
- ID |
- Email |
- Status |
- Welcome Email |
- Created At |
- Updated At |
-
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ {{
+ asc: " 🔼",
+ desc: " 🔽",
+ }[header.column.getIsSorted() as string] ?? null}
+ |
+ ))}
+
+ ))}
- {subscribers.map((subscriber) => (
-
- {subscriber.id} |
- {subscriber.email} |
-
-
- {subscriber.subscribed ? "Subscribed" : "Unsubscribed"}
-
- |
-
-
- {subscriber.welcome_email_sent ? "Sent" : "Pending"}
-
- |
-
- {new Date(subscriber.created_at).toLocaleString()}
- |
-
- {new Date(subscriber.updated_at).toLocaleString()}
- |
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ |
+ ))}
))}
diff --git a/zettelkasten-front/src/pages/admin/AdminUserIndex.tsx b/zettelkasten-front/src/pages/admin/AdminUserIndex.tsx
index d433eaa..72c92e7 100644
--- a/zettelkasten-front/src/pages/admin/AdminUserIndex.tsx
+++ b/zettelkasten-front/src/pages/admin/AdminUserIndex.tsx
@@ -1,11 +1,31 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
import { getUsers } from "../../api/users";
import { User } from "../../models/User";
+import {
+ useReactTable,
+ getCoreRowModel,
+ getSortedRowModel,
+ getFilteredRowModel,
+ flexRender,
+ createColumnHelper,
+ FilterFn,
+ SortingState,
+ ColumnDef,
+} from "@tanstack/react-table";
+import { Link } from "react-router-dom";
+import { rankItem } from "@tanstack/match-sorter-utils";
-import { UserListItem } from "../../components/users/UserListItem"
+const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => {
+ const itemRank = rankItem(row.getValue(columnId), value);
+ addMeta({ itemRank });
+ return itemRank.passed;
+};
export function AdminUserIndex() {
const [users, setUsers] = useState([]);
+ const [sorting, setSorting] = useState([]);
+ const [globalFilter, setGlobalFilter] = useState("");
+
useEffect(() => {
const fetchUsers = async () => {
let tempUsers = await getUsers();
@@ -13,25 +33,154 @@ export function AdminUserIndex() {
};
fetchUsers();
}, []);
+
+ const columnHelper = createColumnHelper();
+
+ const columns = useMemo[]>(
+ () => [
+ columnHelper.accessor("id", {
+ header: "ID",
+ cell: (info) => info.getValue(),
+ }),
+ columnHelper.accessor("username", {
+ header: "Name",
+ cell: (info) => (
+
+ {info.getValue()}
+
+ ),
+ }),
+ columnHelper.accessor("is_admin", {
+ header: "Admin",
+ cell: (info) => (
+
+ {info.getValue() ? "Yes" : "No"}
+
+ ),
+ }),
+ columnHelper.accessor("last_login", {
+ header: "Last Login",
+ cell: (info) => new Date(info.getValue()).toLocaleString(),
+ }),
+ columnHelper.accessor("email", {
+ header: "Email",
+ cell: (info) => info.getValue(),
+ }),
+ columnHelper.accessor("email_validated", {
+ header: "Email Validated",
+ cell: (info) => (
+
+ {info.getValue() ? "Verified" : "Pending"}
+
+ ),
+ }),
+ columnHelper.accessor("stripe_subscription_status", {
+ header: "Subscription",
+ cell: (info) => (
+
+ {info.getValue()}
+
+ ),
+ }),
+ columnHelper.accessor("created_at", {
+ header: "Created At",
+ cell: (info) => new Date(info.getValue()).toLocaleString(),
+ }),
+ columnHelper.accessor("card_count", {
+ header: "Cards",
+ cell: (info) => info.getValue(),
+ }),
+ ],
+ []
+ );
+
+ const table = useReactTable({
+ data: users,
+ columns,
+ state: {
+ sorting,
+ globalFilter,
+ },
+ onSortingChange: setSorting,
+ onGlobalFilterChange: setGlobalFilter,
+ globalFilterFn: fuzzyFilter,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ });
+
return (
-
-
-
- id |
- name |
- is_admin |
- last_login |
- email |
- email_validated |
- stripe_subscription_status |
- created_at |
- cards |
-
- {users &&
- users.map((user, index) => (
-
- ))}
-
+
+
+
Users
+ setGlobalFilter(e.target.value)}
+ className="px-4 py-2 border rounded-lg"
+ placeholder="Search all columns..."
+ />
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ {{
+ asc: " 🔼",
+ desc: " 🔽",
+ }[header.column.getIsSorted() as string] ?? null}
+ |
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ |
+ ))}
+
+ ))}
+
+
+
);
}