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..." + /> +
- - - - - - - - + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} - {subscribers.map((subscriber) => ( - - - - - - - + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} ))} 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 ( -
-
IDEmailStatusWelcome EmailCreated AtUpdated At
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {{ + asc: " 🔼", + desc: " 🔽", + }[header.column.getIsSorted() as string] ?? null} +
{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()} -
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
- - - - - - - - - - - - {users && - users.map((user, index) => ( - - ))} -
idnameis_adminlast_loginemailemail_validatedstripe_subscription_statuscreated_atcards
+
+
+

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) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {{ + asc: " 🔼", + desc: " 🔽", + }[header.column.getIsSorted() as string] ?? null} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
); }