feat: create Recent Edits component

This commit is contained in:
Thomas Bishop 2025-07-23 14:40:41 +01:00
parent 5507e9aa02
commit 5b77317b5f
8 changed files with 372 additions and 82 deletions

34
package-lock.json generated
View file

@ -16,6 +16,7 @@
"@tailwindcss/vite": "^4.1.4",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -2112,6 +2113,39 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.21.3"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",

View file

@ -18,6 +18,7 @@
"@tailwindcss/vite": "^4.1.4",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

View file

@ -12,7 +12,6 @@ export default function EntriesListSidebar() {
queryFn: () => api.get("/entries").then((res) => res.data),
})
console.log(entries)
return (
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
@ -30,7 +29,7 @@ export default function EntriesListSidebar() {
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{entries?.entries.map((item, i) => (
{entries?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<a>
<span className="text-xs">{item.title.replace(/_/g, " ")}</span>

View file

@ -0,0 +1,104 @@
import {
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
} from "@tanstack/react-table"
import { Button } from "./ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export function RecentEditsDataTable({ columns, data, loading }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
const pageCount = table.getPageCount()
const currentPage = table.getState().pagination?.pageIndex
return (
<div>
<div className="rounded-none border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : loading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading...
</TableCell>
</TableRow>
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-between space-x-2 py-4">
<div>
<span className="text-sm text-muted-foreground">{`${currentPage + 1} of ${pageCount}`}</span>
</div>
<div>
<Button
className="rounded-none"
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
className="rounded-none"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
)
}

View file

@ -8,41 +8,41 @@ import api from "../api/eolas-api"
import { Badge } from "./ui/badge"
export default function TagListSidebar() {
const { data: tags, isLoading } = useQuery({
queryKey: ["tag_list"],
queryFn: () => api.get("/tags").then((res) => res.data),
})
const { data: tags, isLoading } = useQuery({
queryKey: ["tag_list"],
queryFn: () => api.get("/tags").then((res) => res.data),
})
console.log(tags)
console.log(tags)
return (
<Collapsible className="group/collapsible">
<SidebarMenuItem key="tags">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<Tags />
<span>Tags</span>
<Badge className="ml-0" variant="secondary">
{tags?.count}
</Badge>
return (
<Collapsible className="group/collapsible">
<SidebarMenuItem key="tags">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<Tags />
<span>Tags</span>
<Badge className="ml-0" variant="secondary">
{tags?.count}
</Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{tags?.tags.map((item, i) => (
<SidebarMenuItem key={i}>
<a>
<span className="text-xs">{item}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{tags?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<a>
<span className="text-xs">{item}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
}

114
src/components/ui/table.tsx Normal file
View file

@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View file

@ -0,0 +1,54 @@
import { RecentEditsDataTable } from "@/components/RecentEditsDataTable"
import { useQuery } from "@tanstack/react-query"
import api from "../api/eolas-api"
const columns = [
{
accessorKey: "title",
header: "Title",
},
{
accessorKey: "date",
header: "Date",
},
{
accessorKey: "time",
header: "Time",
},
]
export default function RecentEdits() {
const { data, isLoading } = useQuery({
queryKey: ["entries_recent"],
queryFn: () => api.get("/entries?limit=20&sort=date").then((res) => res.data),
})
console.log(data)
const parsed = data?.data?.map((entry) => {
const [date, time] = entry?.last_modified?.split(" ")
return {
title: entry.title.replace(/_/g, " "),
date: new Date(date).toLocaleString("en-GB", {
day: "numeric",
month: "long",
year: "numeric",
}),
time: time,
}
})
return (
<>
<div className="border w-full">
<div className="border-b py-2 px-4 lg:px-6 bg-sidebar">
<h2 className="scroll-m-20 font-semibold">Recent edits</h2>
</div>
<div className="p-4 lg:p-6">
<div className="container mx-auto py-2">
<RecentEditsDataTable columns={columns} data={parsed || []} loading={isLoading} />
</div>
</div>
</div>
</>
)
}

View file

@ -1,36 +1,29 @@
import { Card, CardContent } from "@/components/ui/card"
import Main from "@/templates/Main"
// const scoreData = [
// { label: "Total Posts", count: "548" },
// { label: "Tags", count: "68" },
// { label: "Backlinks", count: "766" },
// ]
import RecentEdits from "@/containers/RecentEdits"
export default function Home() {
return (
<>
<Main pageTitle="Home">
{/*
<div className="grid grid-cols-3 gap-4 p-6 pb-0">
{scoreData.map((item, index) => (
<ScoreCard key={index} label={item.label} count={item.count} />
))}
return (
<>
<Main pageTitle="Home">
<div className="flex-1 flex flex-col overflow-auto">
<div className="@container/main flex flex-col">
<div className="p-4 lg:p-6 flex flex-1">
<div className="border w-full">
<div className="border-b py-2 px-4 lg:px-6 bg-sidebar">
<h2 className="scroll-m-20 font-semibold">Welcome</h2>
</div>
<div className="p-4 lg:p-6">
<p className="leading-6 [&:not(:first-child)]:mt-6 font-normal">
I'm Thomas. Eólas is my technical knowledge management system, or
"second-brain", comprising notes from my study of software engineering
and computer science.
</p>
</div>
</div>
</div>
*/}
<div className="flex-1 flex flex-col overflow-auto">
<div className="@container/main flex flex-col">
<div className="p-4 lg:p-6 flex-1 flex">
<Card className="border rounded-none shadow-none w-full p-2">
<CardContent className="p-4 pt-6 overflow-auto">
<h2 className="scroll-m-20 text-xl font-semibold">Welcome</h2>
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
I'm Thomas. Eólas is my technical knowledge management system, or
"second-brain", comprising notes from my study of software engineering
and computer science.
</p>
{/*
<div className="bg-muted rounded-none p-3 font-medium text-sm text-muted-foreground my-4 mb-6">
<p className="">
{" "}
@ -39,22 +32,13 @@ export default function Home() {
</p>
</div>
<h2 className="scroll-m-20 text-lg font-semibold">Recent edits</h2>
</CardContent>
</Card>
</div>
</div>
</div>
</Main>
</>
)
*/}
<div className="px-4 lg:px-6 flex-1 flex">
<RecentEdits />
</div>
</div>
</div>
</Main>
</>
)
}
// const ScoreCard = ({ label, count }) => (
// <div className="rounded-sm border bg-card text-card-foreground p-6">
// <div className="flex flex-col space-y-1.5">
// <h3 className="text-2xl font-semibold leading-none tracking-tight">{count}</h3>
// <p className="text-sm text-muted-foreground">{label}</p>
// </div>
// </div>
// )