Skip to content

Commit

Permalink
admin: adding sortable tables
Browse files Browse the repository at this point in the history
  • Loading branch information
NickSavage committed Jan 3, 2025
1 parent 27ccc79 commit e38a8fd
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 74 deletions.
57 changes: 57 additions & 0 deletions zettelkasten-front/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions zettelkasten-front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 34 additions & 12 deletions zettelkasten-front/src/components/users/UserListItem.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,17 +8,40 @@ interface UserListItemProps {

export function UserListItem({ user }: UserListItemProps) {
return (
<tr>
<td>{user["id"]}</td>
<td>
<Link to={`/admin/user/${user.id}`}>{user.username}</Link>
<tr className="border-b hover:bg-gray-100">
<td className="py-2 px-4">{user.id}</td>
<td className="py-2 px-4">
<Link to={`/admin/user/${user.id}`} className="text-blue-600 hover:text-blue-800">
{user.username}
</Link>
</td>
<td className="py-2 px-4">
<span className={`px-2 py-1 rounded text-sm ${
user.is_admin ? "bg-purple-100 text-purple-800" : "bg-gray-100 text-gray-800"
}`}>
{user.is_admin ? "Yes" : "No"}
</span>
</td>
<td className="py-2 px-4">{new Date(user.last_login).toLocaleString()}</td>
<td className="py-2 px-4">{user.email}</td>
<td className="py-2 px-4">
<span className={`px-2 py-1 rounded text-sm ${
user.email_validated ? "bg-green-100 text-green-800" : "bg-yellow-100 text-yellow-800"
}`}>
{user.email_validated ? "Verified" : "Pending"}
</span>
</td>
<td className="py-2 px-4">
<span className={`px-2 py-1 rounded text-sm ${
user.stripe_subscription_status === "active" ? "bg-green-100 text-green-800" :
user.stripe_subscription_status === "trialing" ? "bg-blue-100 text-blue-800" :
"bg-red-100 text-red-800"
}`}>
{user.stripe_subscription_status}
</span>
</td>
<td>{user["is_admin"] ? "Yes" : "No"}</td>
<td>{user["last_login"]}</td>
<td>{user["email"]}</td>
<td>{user["email_validated"] ? "Yes" : "No"}</td>
<td>{user["stripe_subscription_status"]}</td>
<td>{user["card_count"]}</td>
<td className="py-2 px-4">{new Date(user.created_at).toLocaleString()}</td>
<td className="py-2 px-4">{user.card_count}</td>
</tr>
);
}
167 changes: 125 additions & 42 deletions zettelkasten-front/src/pages/admin/AdminMailingListPage.tsx
Original file line number Diff line number Diff line change
@@ -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<any> = (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
addMeta({ itemRank });
return itemRank.passed;
};

export function AdminMailingListPage() {
const [subscribers, setSubscribers] = useState<MailingListSubscriber[]>([]);
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState("");

useEffect(() => {
const fetchSubscribers = async () => {
Expand All @@ -16,54 +36,117 @@ export function AdminMailingListPage() {
fetchSubscribers();
}, []);

const columnHelper = createColumnHelper<MailingListSubscriber>();

const columns = useMemo<ColumnDef<MailingListSubscriber, any>[]>(
() => [
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) => (
<span
className={`px-2 py-1 rounded text-sm ${
info.getValue()
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{info.getValue() ? "Subscribed" : "Unsubscribed"}
</span>
),
}),
columnHelper.accessor("welcome_email_sent", {
header: "Welcome Email",
cell: (info) => (
<span
className={`px-2 py-1 rounded text-sm ${
info.getValue()
? "bg-blue-100 text-blue-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{info.getValue() ? "Sent" : "Pending"}
</span>
),
}),
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 (
<div className="container mx-auto px-4">
<h1 className="text-2xl font-bold mb-4">Mailing List Subscribers</h1>
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold">Mailing List Subscribers</h1>
<input
type="text"
value={globalFilter ?? ""}
onChange={(e) => setGlobalFilter(e.target.value)}
className="px-4 py-2 border rounded-lg"
placeholder="Search all columns..."
/>
</div>
<div className="overflow-x-auto">
<table className="min-w-full bg-white shadow-md rounded">
<thead className="bg-gray-800 text-white">
<tr>
<th className="py-2 px-4 text-left">ID</th>
<th className="py-2 px-4 text-left">Email</th>
<th className="py-2 px-4 text-left">Status</th>
<th className="py-2 px-4 text-left">Welcome Email</th>
<th className="py-2 px-4 text-left">Created At</th>
<th className="py-2 px-4 text-left">Updated At</th>
</tr>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="py-2 px-4 text-left cursor-pointer select-none"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: " 🔼",
desc: " 🔽",
}[header.column.getIsSorted() as string] ?? null}
</th>
))}
</tr>
))}
</thead>
<tbody>
{subscribers.map((subscriber) => (
<tr key={subscriber.id} className="border-b hover:bg-gray-100">
<td className="py-2 px-4">{subscriber.id}</td>
<td className="py-2 px-4">{subscriber.email}</td>
<td className="py-2 px-4">
<span
className={`px-2 py-1 rounded text-sm ${
subscriber.subscribed
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{subscriber.subscribed ? "Subscribed" : "Unsubscribed"}
</span>
</td>
<td className="py-2 px-4">
<span
className={`px-2 py-1 rounded text-sm ${
subscriber.welcome_email_sent
? "bg-blue-100 text-blue-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{subscriber.welcome_email_sent ? "Sent" : "Pending"}
</span>
</td>
<td className="py-2 px-4">
{new Date(subscriber.created_at).toLocaleString()}
</td>
<td className="py-2 px-4">
{new Date(subscriber.updated_at).toLocaleString()}
</td>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-b hover:bg-gray-100">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="py-2 px-4">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
Expand Down
Loading

0 comments on commit e38a8fd

Please sign in to comment.